抢占式执行是线程是并发执行的主要问题,但是对此我们无法改变,只能保证程序以任意线程顺序运行都能产生正确的结果
这种操作产生的问题通常伴随着操作不是原子的问题,详细见下
操作不是原子指的是操作指令不具有完整性,即操作a后操作b前面不能插入其它操作才能视作操作a和b连在一起的操作是原子的。举个例子:
如果有两个用户同时买票,由于多线程共享一个数据,它们必然会一起看到票数都有一张,如果他们一起买票就会造成票多买一张。如何避免这种情况呢?很简单,只要让a在买票时b在一旁等着就好了,这样就使得买票这个操作具有原子性,这样的操作我们成为“加锁”。具体加锁方法将在下面叙述。
同时即使是一条java语句也不具有原子性,因为一段java语句往往涉及多个CPU操作,如下面代码:
class Counter{ public int num = 0; public void increase(){ num++; } } public class ThreadCounter { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.num); }或许大家会认为结果是10000000,但事实上结果为1 ~ 10000000之间的一个数。
这是因为对于num++这个操作在CPU中有三个操作:
- 从内存中读取数据
- 把数据更新
- 把数据写入内存
如果有两个线程同时进行,这些操作就会进行重叠,如下图:
可见两个操作过后num只加了一次,想要解决这个问题也只要让该操作是原子即可,表现为a在进行increase时b就在一边等着,使得操作变为线性操作,这样就解决了该问题,但与此同时就浪费了大量的时间,在工程中我们需要对锁操作进行平衡,以具体需求为准。
- 线程之间的共享变量存储在主内存
- 每个线程都有自己的工作内存
- 当线程要读取一个数据时都是先从主内存拷贝到”工作内存“然后从”工作内存“读取数据
- 当线程要修改一个数据时是现在”工作内存“中修改,之后同步到主内存
之所以要搞那么多内存是因为这里的主内存才是硬件里的内存,而这里的工作内存指的是寄存器和缓存,所以在工作内存中管理数据要比在主内存快几个数量级。
所以如果线程a和b共享一个数据并在两个工作内存中进行修改就很容易产生线程中值不同步的问题。
在java中我们写出的代码被JVM编译时,部分代码顺序会被改变以优化程序执行效率,在单线程中这种逻辑很好控制,但是到了多线程中,JVM对指令的重排序很容易产生各种问题。
public synchronized void increase(){
num++;
}
synchronized有两种使用方法,第一种就是修饰方法块,当修饰方法被synchronized修饰后,当该方法被调用一个线程时就会触发锁机制,在方法结束之前如果又有线程调用该方法,这个线程就会进入阻塞等待。

但是虽然说是"队列",但是当加锁的线程结束解锁离开后,其它线程并不会按照排队顺序进去,而是会依据操作系统调度进入。

可重入
可重入即当一个线程不会自己锁住自己
当一个线程触发一个方法的锁机制时,如果再此调用该方法理论上就会出现”死锁“,因为那个锁是它自己锁的,也就是说它需要自己等待自己解锁,有点像是一个人上完厕所后从窗户出去了,那么它再想进自然进不去:

但是synchronized修饰的代码块会检测尝试访问的对象是否是加锁的对象,如果不是就拒绝访问,如果是让它通过,同时计数器+1。该线程每结束一次方法计数器都会-1,直到计数器为0时锁才会解开。即使是在运行方法时遇到错误提前跳出也没事,因为JVM只看该方法是否结束,这一点要比C++等方便很多。
锁对象
synchronized关键字不仅能修饰方法,也可以修饰代码块:
public void increase(){
synchronized( 锁对象 ){
num++;
}
}
当一个有synchronized修饰的代码块被线程调用时,如果该方法块内的锁对象已经上锁,则该线程进入等待状态。一般来说锁对象我们都会用this,用this修饰的代码块效果和修饰方法的效果是一致的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-muYzv7au-1663659504993)(C:\Users\yang\AppData\Roaming\Typora\typora-user-images\image-20220920141956976.png)]
volatile关键字修饰的变量可以保证内存的可见性
public class VolatileTest {
static class Counter{
public int count = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while(counter.count == 0){
}
System.out.println("循环结束");
});
t1.start();
Scanner scanner = new Scanner(System.in);
counter.count = scanner.nextInt();
}
}
如上代码,理论上当我们从控制台输入一个非零数后程序就会立刻停止,但事实并非是这样。如上面所说,JVM为了优化速度,每个线程都有一个”工作内存“,t1线程开始时从主内存中获取数据存储在工作内存中,然后在工作内存上操作数据,这就导致了当我们在main线程修改数据后t1线程读取的数据仍然是工作内存中没有修改的数据,所以循环永远不会停止。
而volatile关键字修饰后JVM在涉及到count后都会强制从主内存中获取数据,同时在更新数据时JVM会把所有其它工作内存中的数据全部进行更新。这样做减缓了程序的运行速度,但是提高了数据的准确性。
public volatile int count = 0;
volatile关键字防止指令重排序
示例来自单例模式的实现,需要了解可看博主另一篇博客:Java单例模式的完整实现,从0到1带你一步步优化
部分内容如下:
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
代码上虽然两个if内容相同,但是作用是天差地别,第一个if是让instance不为null时直接返回,第二个if是判断在多个线程同时进入第一个if后是否已经有过实例化,如果没实例化就创建,有就结束。
在程序运行时new有三个指令:

在JVM中为了性能优化,可能会把2 和 3两个指令进行调换,这样的调换虽然在单线程中没有影响,但是在多线程中可能出现返回空引用的情况,即线程a先new一个对象,先执行3指令,此时instance已经不为空,但是2还没执行时另一个线程直接调用了getInstance,这就导致了空引用传递,图示如下:

为了避免指令重排序,我们可以使用volatile关键字进行修饰,完整代码如下:
private static volatile SingletonLazy instance = null;//使用volatile修饰,禁止getInstance时指令重排序
private SingletonLazy(){
}
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
在并发编程时我们往往是一个线程a依赖另一个线程b的出的结果才能正常运行,这就需要a先等待,然后让b让它走时它才能走。等待就是wait(),notify()就是唤醒操作。
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
释放当前的锁
之所以释放当前锁是因为在等待时线程已经不用执行了,所以可以先解锁让别人使用,如果不解锁就是占着茅坑不拉屎了。
满足一定条件时被唤醒, 重新尝试获取这个锁.
注意: wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
wait等待结束的条件:
如果多个线程都在wait,notify会随机唤醒一个线程,此外还有一个notifyAll方法唤醒所有线程,但是即使唤醒了所有线程,这些线程还是在锁状态,根据操作系统的调度一个一个运行。