java线程同步inc dec_廖雪峰Java11多线程编程-2线程同步-1同步代码块

1.线程安全问题

多个线程同时运行,线程调度由操作系统决定,程序本身无法决定。

如果多个线程同时读写共享变量,就可能出现问题。

假设有AddThread和DecThread,它们分别对同一个共享变量做加和减运算LOOP次,最终结果应该是0。但某些时候比如LOOP为10000时,结果是错误的。

class AddThread extends Thread{

public void run(){

for(int i=0;i

Main.count += 1;

}

}

}

class DecThread extends Thread{

public void run(){

for(int i=0;i

Main.count -= 1;

}

}

}

public class Main {

final static int LOOP = 10000;

public static int count = 0;

public static void main(String[] args) throws InterruptedException{

Thread t1 = new AddThread();

Thread t2 = new DecThread();

t1.start();

t2.start();

//等待这两个线程执行结束

t1.join();

t2.join();

System.out.println(count);

}

}

8e96b0109de295b1b294ad1197b99b42.png

## 2.原子操作

* 因此对共享变量进行写入时,必须保证是原子操作

* 原子操作是指不能被中断的一个或一系列操作

当执行 n = n +1时,编译器会把它编译为3条字节码指令,分别是ILOAD, IADD, ISTORE。所以对于这个简单的赋值语句,它并不是一个原子操作,这就可能导致两个线程在执行这条语句的时候,会出现问题。

假设1:n=100,Thread1执行语句n为101,Thread2再执行n为102。

假设2:Thread1刚执行完ILOAD指令,就被操作系统暂停了,然后Thread2调度执行,结果n变成了101,此后Thread1再度被操作系统调度执行,结果也是101。即n+1的指令被2个线程调用了2次,最终只加了1.

7bdf0ba5910d50305b3a6747b04a08a4.png

所以我们要保证当Thread1执行时,Thread2不能执行,直到Thread1执行完毕,Thread2才能开始执行。这样运行的结果就是正确的。

要实现这个效果,就要对ILOAD之前和ISTORE之后进行加锁和解锁。

d98d411a57b6e59268e17227cab7e6f4.png

3.同步代码块

Java使用synchronized对一个对象进行加锁:

为了保证一系列操作作为原子操作,必须保证一系列操作过程中不被其他线程执行

synchronized (lock){

n=n+1;

}

当一个线程想要执行synchronized语句块时,必须首先获得指定对象的锁,这个对象就是synchronized括号里的对象,然后线程再执行synchronized语句块,执行结束以后释放锁。

在执行synchronized语句块时,如果Thread1执行到任何语句时,被操作系统中断。其他线程如Thread2因为无法获取lock对象的锁,从而导致Thread2无法进入synchronized语句块,Thread2就必须等待,直到Thread1再次被调用,并执行完synchronized语句块释放了锁,Thread2才能获得lock对象锁,进入synchronized语句块。

synchronized保证了代码块和任意时刻最多只有一个线程能执行。因为一个对象的锁只能被一个线程获得,其他线程必须等待。

synchronized的问题:性能下降。因为synchronized代码块无法并发执行,所以性能会下降。此外加锁和解锁都会消耗一定的时间,所以synchronized会降低程序的执行效率。

如何使用synchronized:1.找出修改共享变量的线程代码块

2.选择一个实例作为锁

3.使用synchronized(lock Object){...}

注意:对于同一个变量的修改,必须要获取同一个锁,如果2个线程获取的是不同的锁,它们是没有办法进行同步的。

不用担心异常。无论有无异常,在synchronized结束时都会释放锁。

class AddThread extends Thread{

public void run(){

for(int i=0;i

synchronized (Main.LOCK) {

Main.count += 1;

}

}

}

}

class DecThread extends Thread{

public void run(){

for(int i=0;i

synchronized (Main.LOCK) {//对于同一个变量的修改,要使用同一个锁

Main.count -= 1;

}//无论有无异常,都会在此释放锁

}

}

}

public class Main {

final static int LOOP = 10000;

public static int count = 0;

public static final Object LOCK = new Object();

public static void main(String[] args) throws InterruptedException{

Thread t1 = new AddThread();

Thread t2 = new DecThread();

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println(count);

}

}

180b2fe5ee80349d16da475fa859efbf.png

4.JVM的原子操作

JVM定义了几种原子操作:

基本类型(long和double除外)赋值

引用类型赋值

注意:原子操作时不需要同步的。

可以把非原子操作变为原子操作

局部变量不需要同步

//原子操作不需要同步

public void set(int m){

synchronized (obj){

this.value = m;

}

}

//->

public void set(int m){

this.value = m;

}

//对2个int类型进行赋值,它不是一个原子操作。但可以先构造一个int数组,然后利用引用类型赋值,把它变成1个原子操作。

class Pair{

int first;

int last;

public void set(int first,int last){

synchronized (this){

this.first = first;

this.last = last;

}

}

}

//->

class Pair{

int[] pair;

public void set(int first,int last){

int[] ps = new int[]{first,last};

this.pair = ps;

}

}

//a,b,s1,s2,r都是局部变量,各个线程的局部变量是完全独立的,互不影响,所以这个方法不需要同步。

public int avg(int a, int b){

int s1 = a*a + b*b;

int s2 = a + b;

int r = s1/s2;

return r;

}

5.总结:

多线程同时修改变量,会造成逻辑错误

* 需要通过synchronized同步

* 同步的本质就是给指定对象加锁

* 注意加锁对象必须是同一个实例

对JVM定义的单个原子操作不需要同步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值