线程是进程执行的一个路线。在同一个进程中的,线程共享进程的内存空间,线程间可以自由切换,并发执行。在Java开发的过程中,设计的应用程序,往往都是多个线程同时工作的,这样能够提高程序的执行效率,但是也带来了编程的复杂,需要解决多线程之间资源分配的问题。在本篇的博客中,主要记录的是Java实现多线程的几个方式,以及基础的概念和常用的方法操作。
继承Thread类,重写run方法
实现Runnable接口,编写run方法
实现Callable接口,编写call方法
// 定义MyThread继承Thread类型,并重写run方法,在run方法内编写创建的线程要执行的任务。
public class MyThread extends Thread {
// 线程要执行的方法
// 通过thread对象的start方法执行,是一条新的执行路径
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("窗前明月光"+i);
}
}
}
// 创建MyThread类,并调用start方法,然后线程会开始执行run方法
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i=0;i<100;i++){
System.out.println("疑是地上霜"+i);
}
}
通过上面运行的程序可以观察到,myThread执行的任务和主线程打印任务是有交叉执行的,说明Java中线程之间的运行是抢占式的,线程抢到资源后,可以执行,没有抢到足够资源的线程需要等待下一次的抢占。
// 定义MyRunnable实现Runnable接口,并实现run方法,run方法内编写线程执行的任务。
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i=0;i<20;i++){
System.out.println("锄禾日当午"+i);
}
}
}
// 将MyRunnable对象作为创建Thread对象的参数,然后调用start方法执行线程
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i=0;i<20;i++){
System.out.println("汗滴禾下土"+i);
}
}
特点:
1. 通过创建任务,给线程分配任务,更适合多个线程同时执行相同的任务;
2. 可以避免单继承的局限性;
3. 任务和线程本身是分离,提高程序的健壮性
4. 线程池的操作,只支持实现Runnable接口的类型的任务,不接受继承Thread的线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建MyCallable对象
MyCallable myCallable = new MyCallable();
// 2. 创建FutureTask对象,并将MyCallable对象作为创建对象的参数
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 3. 调用Thread对象的start方法执行线程
new Thread(futureTask).start(); // 执行线程
// 4. 可以通过get方法获取线程执行完成时的返回值
System.out.println(futureTask.get()); // 输出任务返回值
}
特点: 可以携带线程任务执行完成后返回的参数
// 设置当前线程的名字
Thread.currentThread().setName("线程1");
// 获取当前线程的名字
Thread.currentThread().getName();
Thread.sleep(1000); // 单位是ms
// 终端线程执行可以调用interrupt()函数,通过中断可以终止执行的线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000);
}
t1.interrupt(); // 中断t1线程,通知他进行其他操作,可以是终止自身线程,或者是进行其他的操作
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
// 接收到中断信号时,终止任务/或者开始其他的任务
System.out.println("接收到中断信号时,终止任务");
return; // 终止任务
}
}
}
}
设置守护线程
通过调用线程对象的setDaemon()方法,并传入true可以设置线程为守护线程
Java中的线程可以分成用户线程和守护线程,下面是这两种线程的特点;
用户线程,当进程中没有一个用户线程的时候,进程结束
守护线程,守护用户线程,当最后一个用户线程结束时,所有的守护线程死亡
举例如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable());
t1.setDaemon(true); // 设置t1为守护线程,如果所有的用户线程结束了,t1守护线程也会停止执行
t1.start();
for (int i=0;i<5;i++){
// 比较快结束
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000);
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
// 在所有用户线程结束时,作为守护线程的它也会结束
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
在多线程的环境下,线程的执行往往是多样的,当这多个线程在同时对一个数据进行更新操作的时候,可能会因为同时操作的原因,导致数据出现错误,不满足实际的情况。在这种情况下,如果需要保证数据的安全可靠,就要是每个对数据进行更新的线程排队执行,这样才能保证数据不会超出合理的范围,保证每次操作的合理性。这种排队的行为是线程同步的一种具体解释.
Java中为了保证线程安全,使线程同步执行的方式有以下三种:
同步代码块
同步方法
显示锁Lock
下面详细介绍这三种的使用方法,并给出一些例子:
首先给出一个会出现线程不安全的情况,这个例子演示的是多个线程同时进行买票,最后会出现剩余票数是负数的情况,这是不合理的现象。示例如下:
// 模拟线程不安全的情景:买票线程
public static void main(String[] args) {
Ticekt ticekt = new Ticekt(); // 一个对象
new Thread(ticekt).start(); // 三个买票线程
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10; // 余票数量
@Override
public void run() {
while (num>0){
// 票数>0,继续卖
System.out.println("准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("出票成功!");
num--;
System.out.println("当前余票数:"+num);
}
}
}
同步代码块使用关键字synchronize,参数是一个锁对象,然后将同步执行的代码块用{}包围。当遇到同一个锁对象的时候,这些线程会同步执行,如果使不同的锁对象的时候没有任何影响。因此,如果希望同步执行的话,要注意锁对象要是同一个对象。
修改上面不安全的买票程序,做出下面的例子:
// synchronize,利用锁对象,
// 当要进入同一个锁的代码块的时候会先检查这个锁有没有被占用,
// 被占用则等待,否则占用锁进入代码块
public static void main(String[] args) {
Ticekt ticekt = new Ticekt();
new Thread(ticekt).start();
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10;
private Object o =new Object(); // 锁对象
@Override
public void run() {
while (true){
synchronized (o){
// 同步代码块开始
if (num>0){
System.out.println("准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("出票成功!");
num--;
System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
}else
break;
} // 同步代码块结束
}
}
}
同步方法的实现和上面的同步代码块类似,也使用synchronized,但是它修饰的对象变成了方法。当修饰的是普通方法的时候,锁对象是当前对象本身,即this;如果修饰的是静态方法的时候,锁对象则是这个类的字节码文件对象。
下面只做修饰普通方法的举例,修饰静态方法的情景类似:
public static void main(String[] args) {
Ticekt ticekt = new Ticekt();
new Thread(ticekt).start();
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10;
@Override
public void run() {
while (true){
boolean flag = sale();
if (!flag)
break;
}
}
// 买票方法,同步执行,
// 不是静态方法,锁对象为this,即调用的对象
// 是静态方法,锁对象为Ticke.class,字节码文件对象
// 同一个类的对象会共用this这个锁,可以会影响其他同步方法的执行
public synchronized boolean sale(){
if (num>0){
System.out.println(Thread.currentThread().getName()+":准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":出票成功!");
num--;
System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
return true;
}
return false;
}
}
同步代码块和同步方法实现都是基于隐式锁。
使用实现接口Lock的类ReentrantLock的方式是基于显示锁。
显示锁比较适合自定义加锁和解锁,带来个性化的同时也可能会带来加锁后忘解锁的情况,在使用的过程中,需要充分考虑到加锁后在哪个地方再解锁,防止程序进入死锁,无法释放资源。下面是使用案例,也是基于买票程序的:
public static void main(String[] args) {
Ticekt ticekt = new Ticekt();
new Thread(ticekt).start();
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10;
// 用实现接口Lock的类ReentrantLock创建对象
private Lock l= new ReentrantLock();
@Override
public void run() {
while (true){
l.lock(); // 上锁资源访问
if (num>0){
System.out.println(Thread.currentThread().getName()+":准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":出票成功!");
num--;
System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
l.unlock(); // 操作成功,解锁
}else{
l.unlock(); // 没有买票,break前需要释放锁,防止后面的线程被锁住不能释放
break;
}
}
}
}
以上的介绍,总结了Java中使用多线程的基本内容,包括线程的创建的方式,线程常用的方法,实现线程同步的方法。在我们实际开发中,情景往往都是复杂的,可能会有大量的请求会同时发出,这个时候就需要合理的处理号这些请求,利用多线程的思想去解决这些问题。认识并使用多线程后,能够使自己开发的程序更加符合实际开发的需求,提高程序的执行效率。Java中的多线程的知识还很多,比如线程池。上面的介绍只是基础入门,想要深入学习的话,可以继续查找资料,或者找些项目实践。