【Java】【线程同步】Java线程同步全API详解及代码实测

前言

这篇博客重点在于讲解API功能细节,对于同步异步还没有清晰概念,或从未使用过线程同步相关API的同学,请先自行补全入门基础,这里不再累述

由于Java为线程API都设置了强制异常检查,所以编程时需要编写大量的try-catch代码,为了节省这些无意义代码,让博客更简洁清晰,我们封装了一个Threads工具类,来屏蔽这些异常检查代码。比如:

  • Threads.post(runnable)相当于new Thread(runnable).start()
  • Threads.sleep()相当于Thread.sleep()
  • Threads.yield()相当于Thread.yield()
  • Threads.wait(lock)相当于lock.wait()
  • Threads.notify(lock)相当于lock.notify()
  • Threads.join(thread)相当于thread.join()
  • Threads.interrupt(thread)相当于thread.interrupt()

博客中提到的lock对象,均是指作为同步锁的对象,任意对象都可以作为同步锁

线程状态

出于线程管理和描述的需要,我们必须清楚地定义出线程的所有状态或生命周期

  • 新建状态(NEW):thread刚被new出来,还没有执行start方法
  • 就绪状态(READY):thread已经在运行,但是尚未获得CPU或同步锁资源
  • 运行状态(RUNNABLE):thread获得了CPU和同步锁资源,正在执行代码
  • 阻塞状态(BLOCKED):thread因为sychronized,sleep,wait,join等原因,暂时停止执行(等待条件成立时,就会复原到就绪状态)
  • 中止状态(TERMINATED):thread的run方法执行完毕,任务完成,线程结束

Thread.sleep(long ms)

让当前线程休眠一段时间,达到指定时间后,再继续执行后续代码

synchronized(lock) {…}

同步块,表示在{…}块作用域内,当前线程获得lock对象的访问和修改权限,也可以形象地说成是,当前线程获得lock对象锁
在{…}块结束之前,其它线程无法访问或修改lock对象,需要一直等待

这样说其实并不严谨,这只是最简单的情况,通过Thread.wait等方法可以让同步块暂时让出对线锁,马上就会提到

synchronized function() {…}

同步方法,同一时间只有一个线程可以进入被synchronized修饰的方法,只有当前线程退出方法后,其它线程才能进入,相当于是一个方法锁

lock.wait()

只有在同步块中,lock被作为同步锁时,才能使用此方法
这个方法让当前线程临时让出lock的所有权,当前线程进入阻塞状态
直到其它线程调用了lock.notify()或lock.notifyAll()通知资源已经释放,才会恢复到就绪状态,重新竞争资源

代码测试


	//同步锁
	final Object lock = new Object();
	
	//启动线程A
	Threads.post(() -> {
	    synchronized (lock) {   //1.由于线程B休眠,所以线程A先进入同步块,获得lock所有权
	        Threads.sleep(5000); //2.线程A休眠,保持对lock的所有权
	        System.out.println("A1");
	        Threads.waits(lock);    //4.线程A让出lock所有权,等待其它线程notify,再重新竞争lock所有权
	        System.out.println("A2");   //由于两个线程都没有调用notify,所以A2和B2永远不会打印,一直阻塞在wait处
	    }
	});
	
	//启动线程B
	Threads.post(() -> {
	    Threads.sleep(2000);
	    synchronized (lock) {   //3.由于lock被线程A占有,进入阻塞状态,等待lock资源
	        System.out.println("B1");   //5.线程B获得lock所有权
	        Threads.waits(lock);    //6.线程B让出lock所有权,等待其它线程notify,再重新竞争lock所有权
	        System.out.println("B2");
	    }
	});

lock.wait(long ms)

同wait()功能一样,但是增加了超时处理
当超出timeout指定的时间时,即使其它线程没有调用lock.notify(),线程也会自动进入就绪状态,重新竞争lock资源

代码测试


	//同步锁
	final Object lock = new Object();
	
	//启动线程A
	Threads.post(() -> {
	    synchronized (lock) {   //1.由于线程B休眠,所以线程A先进入同步块,获得lock所有权
	        Threads.sleep(5000); //2.线程A休眠,保持对lock的所有权
	        System.out.println("A1");
	        Threads.waits(lock, 5000);    //4.线程A让出lock所有权,等待其它线程notify或超时,再重新竞争lock所有权
	        System.out.println("A2");   //5.wait超时,线程自动切换到就绪状态,获得lock所有权
	    }
	});
	
	//启动线程B
	Threads.post(() -> {
	    Threads.sleep(2000);
	    synchronized (lock) {   //3.由于lock被线程A占有,进入阻塞状态,等待lock资源
	        System.out.println("B1");   //5.线程B获得lock所有权
	        Threads.waits(lock);    //6.线程B让出lock所有权,等待其它线程notify,再重新竞争lock所有权
	        System.out.println("B2");   //由于线程A没有调用notify,所以B2永远不会打印
	    }
	});

sleep和wait方法的区别

  • sleep是Thread类的方法,wait是Object类的方法
  • sleep(休眠)是当前线程什么都不做,和其它线程互不影响,没有如何互动
  • wait(等待)则是交出同步锁所有权,并且等待其它线程的通知,wait是围绕着同步锁进行的线程间互动

lock.notify()

通知另一个正处于wait状态的线程退出wait状态,进入就绪状态,开始竞争lock资源
如果有多个线程都处于wait状态,具体哪个线程被通知是不确定的,由系统调度决定
还有一点非常重要的就是,线程调用notify,并不意味着就立刻释放lock锁,仅仅是通知其它线程而已,其它线程也只是开始竞争资源,并不代表可以立刻得到lock资源,只有notify的线程退出同步块之后,lock资源才会被释放,其它线程才有可能抢到资源
一般建议将notify放在同步块的最后一行代码执行,因为就算提前执行也没用,反而容易引起误会或重复调用

lock.notifyAll()

和nofify方法功能一致,只不过nofify方法只通知一个线程,而notifyAll通知所有处于wait状态的线程

代码测试


	//同步锁
	final Object lock = new Object();
	
	//由于多个线程是并发运行的,没法控制哪个先执行,这样就不方便测试
	//但是我们可以通过休眠,来控制有效代码的执行顺序
	//我们让三个线程分别休眠1s,2s,3s,这样代码执行顺序就是A-B-C
	
	//启动线程A
	Threads.post(() -> {
	    Threads.sleep(100);
	    synchronized (lock) {
	        System.out.println("A1");   //1.线程A获得同步锁,开始执行代码
	        Threads.wait(lock); //2.线程A交出同步锁,进入wait状态
	        System.out.println("A2");
	    }
	});
	
	//启动线程B
	Threads.post(() -> {
	    Threads.sleep(200);
	    synchronized (lock) {
	        Threads.sleep(500);    //3.线程B获得同步锁,开始执行代码
	        System.out.println("B1");
	        Threads.wait(lock); //5.线程B交出同步锁,进入wait状态
	        System.out.println("B2");
	    }
	});
	
	//启动线程C
	Threads.post(() -> {
	    Threads.sleep(300);
	    synchronized (lock) {   //4.由于同步锁被B占有,无法进入同步块
	        System.out.println("C1");   //6.A和B都交出了同步锁,线程C进入同步块,获得同步锁
	        Threads.notify(lock);   //7.通知其中一个wait线程退出阻塞状态,进入就绪状态
	        Threads.sleep(500);    //8.由于线程C仍持有同步锁,wait线程只能继续等待,虽然它已经是就绪状态
	        System.out.println("C2");
	    }   //9.线程C释放同步锁,wait线程获得同步锁,继续执行代码,但由于只通知了一个线程,A2和B2只有一个会打印
	});

Thread.yield()

它告诉系统,可以让当前线程先放弃lock资源,从运行状态转入就绪状态,从而让其它线程有获得lock资源的机会
它是一个建议性的API,并不能保证其它线程一定能得到lock资源
因为就绪状态的线程仍然会竞争资源,可能刚刚让出lock资源,马上又给自己抢到了,但是这样至少保证了其它线程有得到lock资源的可能性
一般在我们不想垄断资源,也不想永远在其它线程之后执行,想让不同线程随机自由竞争的时候,就可以使用这个API
由于它是建议性的,运行效果也是随机的,一般我们并没有必要调用这个方法,往往我们都是出于"完美主义"的想法,想"公平对待"不同线程,才会去使用它

值得注意的是,Thread.sleep()和Thread.yield()都是静态方法,属于Thread类的方法,而不是属于某个对象的方法,它们的操作对象都是当前线程

thread.setPriority(int priority)

提到了Thread.yield方法,就顺便提下thread.setPriority方法,它也是一个建议性的方法
它为线程设置不同的优先级,取值范围为1-10,数值越大优先级越高
高优先级线程有更高概率获得CPU资源和锁资源,但这不是一定生效的,只是给CPU的一个建议

thread.join()

让另一个线程先执行,执行完再回到当前线程继续执行,这个thread一般是其它线程
这个方法不涉及同步锁,仅仅是控制多个线程的执行顺序,但也会让线程进入阻塞状态

thread.join(long ms)

和thread.join()功能一致,但是增加了超时限制,达到指定时间,即使其它线程未执行完毕,也会继续执行

代码测试


	//同步锁
	final Object lock = new Object();
	
	//创建线程B
	Thread t2 = new Thread(() -> {
	    Threads.sleep(3000);    //3.线程B优先执行,线程A等待
	    System.out.println("B1");
	    System.out.println("B2");
	});    //4.线程B执行完毕
	
	//创建线程A
	Thread t1 = new Thread(() -> {
	    System.out.println("A1");   //1.由于线程B在休眠,所以A先执行
	    Threads.join(t2);   //2.等待B执行完毕,再继续执行
	    System.out.println("A2");    //5.线程A继续执行
	});
	
	//启动线程
	t1.start();
	t2.start();

线程阻塞的几种情景

  • 由于sychronized无法获得对线锁,进入阻塞状态,其它线程让出同步锁时,即可打破阻塞状态
  • 由于sleep方法,主动进入阻塞状态,达到超时时间,即可退出阻塞状态
  • 由于wait方法,主动进入阻塞状态,收到其它线程的notify,或达到超时时间,即可打破阻塞状态
  • 由于join方法,主动进入阻塞状态,其它线程执行完毕,或达到超时时间,即可打破阻塞状态

thread.interrupt()

中断一个处于阻塞状态的线程,并抛出一个InterruptedException
注意两点,一是只能中断处于阻塞状态的线程,不能中断处于正常运行状态的线程,二是中断后只是抛出一个异常,并不是直接让线程停止
如果我们正确处理了这个异常,线程还是会继续往下执行,当然,我们也可以在捕获到异常时,通过代码让线程return或跳到最后一行,从而达到停止线程的目的

注意,interrupt()方法只对sleep,wait,join方法引起的阻塞状态有效,对sychronized同步锁造成的阻塞无效

代码测试


	//同步锁
	final Object lock = new Object();
	
	//创建线程C
	Thread t3 = new Thread(() -> {
	    synchronized (lock) {
	        Threads.sleep(100000);
	    }
	});
	
	//创建线程A
	Thread t1 = new Thread(() -> {
	    System.out.println("A1");   //1.线程A和C先运行,因为线程B在休眠
	    try {
	        Threads.join(t3);   //2.等待线程C运行完毕,由于C睡眠时间很长,A会长时间阻塞
	    } catch (Exception e) {     //6.线程A被打断,抛出InterruptedException异常
	        System.out.println("InterruptedException");
	        return;     //7.捕获异常,return结束线程,也可以不结束,取决于代码
	    }
	    System.out.println("A2");   //8.由于线程结束,A2不会被打印
	});
	
	//创建线程B
	Thread t2 = new Thread(() -> {
	    Threads.sleep(1000);
	    System.out.println("B1");   //3.线程B开始运行
	    t1.interrupt();     //4.打断线程A的阻塞状态
	    System.out.println("B2");   //5.线程B继续执行
	});
	
	
	//启动线程
	t1.start();
	t2.start();
	t3.start();

Java源码中对Thread.State的定义

我们在文章的刚开始,就已经讲解过线程状态的定义,但是我们是从线程工作原理的角度来讲的,它适用于所有语言

Java在Thread也定义了一个名为State的内部类,可以通过thread.getState()来获取线程的State,Thread源码中定义的线程状态则和我们上面定义的有所差异

我们先来看下Java源码


	package java.lang;

	public class Thread implements Runnable {

	    public enum State {
	        NEW, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
	    }

		private volatile int threadStatus = 0;
	
	    public State getState() {
	        return sun.misc.VM.toThreadState(threadStatus);
	    }
	}

通过源码我们可以看到以下区别

  • Java源码中没有定义就绪状态,因为就绪状态只存在一瞬间,如果竞争资源成功,马上就会进入运行状态,如果失败,则会立刻转入阻塞状态,从代码实现的角度来说,一瞬间的状态是没实际意义的,因为它的值马上就会发生变化
  • Java源码中将阻塞状态分为了三种:BLOCKED, WAITING, TIMED_WAITING
  • sychronized引起的同步锁阻塞,用BLOCKED表示
  • wait(),join()引起的无限等待阻塞,用WAITING表示
  • wait(ms),join(ms),sleep(ms)引起的限时等待阻塞,用TIMED_WAITING表示
  • 可以看到,和我们在文章的划分其实本质上是一样的,只是表述上的区别。因为源码是为了实现Thread接口功能所设计的,它必须区分每种具体的状态,才能实现wait,notify这些功能

Java内存模型和volatile关键字

Java内存模型我们在这里不详解,只是为了介绍volatile而简单提下
在Java的内存模型中,为了保证速度,每个CPU都有一个高速缓存,线程使用变量时,首先访问的并不是内存中的值,而是CPU缓存中的值。这样就会出现一个问题,当多个线程使用不同CPU的时候,是从不同的CPU缓存中取值,这样就有可能会出现变量值不一致的情况
volatile关键字能够保证被其修饰的变量,在数值被改变时,能及时地反映到内存和各个CPU缓存中,这样就能每个线程获取到的值是最新的

volatile只能保证取值赋值语句在多线程情况下可以正确执行,并不能保证其它情景下变量值也是同步的,甚至是非常简单的语句都不行

volatile的作用非常有限,我们看下这个例子就知道了


	private volatile int sum = 0//这就是我们要举例的语句,非常之简单
	//但是volatile关键字不能保证这条语句能够正确地执行
	sum++;

	//volatile只能保证下面这种形式的语句能够按照预期的结果执行
	sum = 100;
	int result = sum;

	//sum++其实相当于以下语句
	sum = sum + 1;
	//再进一步,它其实相当于这样的语句
	int temp = sum;
	sum = temp + 1;

	//这就是问题的关键所在
	//volatile可以保证int temp = sum的正确性
	//volatile也可以保证sum = temp + 1的正确性
	//但是这是对CPU来说,是两次运算,在两次运算之间,其它线程可能已经修改了sum的值
	
	//比如我们有10000个线程都在这些sum++这个简单的代码
	//我们的线程第一个拿到了sum的值,它的初始值为0,于是
	//int temp = 0
	//sum = temp + 1 = 1;
	//但是在这两句之间,其它的线程可能已经让sum自增了100次,sum的最新值已经变成了100
	//而我们却还在用旧的sum值在做自增运算,得到1,然后赋值给sum,覆盖了其它线程的运算结果

	//这显然不是我们所预期的结果
	//问题的关键就在于,sum++是复合操作,它其实相当于多个运算语句
	//而多个运算语句之间,其它线程是有可能插入进来先执行的,让我们的操作变得无意义
	//所以正如前面所说的,volatile只能保证基本取值赋值语句的正确性

	//从这个例子我们可以看出,volatile的功能其实极其有限
	//不知道大家有没有悟出来一个结论:
	//volatile其实并不是用来解决线程间的语句同步问题的,而是用来解决CPU之间的变量值同步问题

有了sychronized还需要volatile吗

看来上节的说明,不知道大家有没有自己悟出来这样一点:
volatile其实并不是用来解决线程间的语句同步问题的,而是用来解决CPU之间的变量值同步问题

sychronized关键字用于解决线程间的语句同步问题,它将同步块作为一个整体,其它线程只有在整个同步块都退出时,才有可能修改或访问同步变量
除此之外,其实sychronized关键字也具有保证CPU之间变量值同步的功能,sychronized在进入同步块时,会清空所有的CPU缓存,从主内存中重新获得变量值,sychronized在退出同步块时,会将最新的变量值同步到主内存当中

虽然sychronized的功能要强大于volatile,但是由于sychronized是阻塞式的,它会影响到代码的执行速度,一个线程在执行,其它线程就要等待。而且sychronized本身在实现上,就比volatile更加复杂,运行效率更低

所以在一些简单的情况下,比如一个线程只写值,另一个线程只读值,也不用关心多线程下语句的执行顺序时,使用volatile就足够了

代码测试

我们通过代码来测试下,没有volatile关键字,会不会出现变量值不同步的问题


	@SuppressWarnings("all")
	public class Hello {
	
	    public static Integer value = 0;
	
	    public static void main(String[] args) {
	
	        //线程A先执行,value=0
	        Threads.post(() -> {
	            while (true)
	                if (value != 0)
	                    System.out.println("Value Change");
	            //永远不会打印,说明线程B的修改没有反映到线程A中
	            //如果我们在value前加上volatile修饰,则马上打印,说明volatile确实具有同步数值的作用
	
	            //另外,即使我们不使用volatile关键字
	            //如果我们在while (true)里面添加sychronized或sleep语句,发现也会打印语句
	            //这说明,进入sychronized同步块或执行sleep语句后,会自动同步数值

            	//注意:这个测试代码其实是有讲究的,我们不能直接通过打印value的值去测试
	            //因为System.out.println方法本身内部就包含了sychronized代码在里面
	            //如果直接打印,必然会造成变量同步,这样是测不出真实结果的
	        });
	
	        //线程B后执行,修改value的值
	        Threads.post(() -> {
	            Threads.sleep(200);
	            while (true)
	                value = 999;
	        });
	    }
	
	}

工具类代码

补上工具类代码,方便大家做伸手党


	@SuppressWarnings("all")
	public class Threads {
	
	    public static void post(Runnable runnable) {
	        new Thread(runnable).start();
	    }
	
	    public static void sleep(long ms) {
	        try {
	            Thread.sleep(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void wait(Object lock) {
	        try {
	            lock.wait();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void wait(Object lock, long ms) {
	        try {
	            lock.wait(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void notify(Object lock) {
	        try {
	            lock.notify();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void notifyAll(Object lock) {
	        try {
	            lock.notifyAll();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void yield() {
	        try {
	            Thread.yield();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	
	    public static void join(Thread thread) {
	        try {
	            thread.join();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void join(Thread thread, long ms) {
	        try {
	            thread.join(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void interrupt(Thread thread) {
	        try {
	            thread.interrupt();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	}

发布了442 篇原创文章 · 获赞 45 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/u013718730/article/details/104306721