java 全局变量同步机制_Java 多线程 之 线程的同步机制

本文介绍了Java中线程安全问题的产生及其解决方案——同步机制。通过一个电影票售卖的案例展示了线程不安全的现象,如错票和重票,分析了原因并提出了线程同步的概念。Java提供了同步代码块和同步方法两种同步机制,通过`synchronized`关键字实现,确保同一时间只有一个线程访问共享资源。此外,还讨论了Lock锁,如ReentrantLock,作为更灵活的线程同步工具。最后,提到了死锁的问题及其避免策略。
摘要由CSDN通过智能技术生成

一、线程的安全问题

1、问题的发现

当有多个线程同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全的。

下面通过一个案例来演示线程的安全问题。

模拟电影票买票的过程,其中,一共有100张票。下面来模拟电影票的售票窗口,实现多个窗口同时卖票,采用线程对象来模拟,通过实现 Runnable 接口子类来模拟。

示例:

1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4

5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8

9 //同时卖票

10 t1.start();11 t2.start();12 t3.start();13 }14 }15

16 class Window implementsRunnable {17

18 private int ticket = 100;19

20 @Override21 public voidrun() {22

23 while (true) {24 //有票,可以出售

25 if (ticket > 0) {26

27 //出票操作,使用 sleep 模拟一下出票时间

28 try{29 Thread.sleep(100);30 } catch(InterruptedException e) {31 e.printStackTrace();32 }33 //获取当前线程对象的名字

34 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);35

36

37 ticket--;38 } else{39 break;40 }41 }42 }43 }

运行结果,发现会有这样的现象发生:

(1)错票

(2)重票

在运行结果中可以看到会有两个问题发生:

① 相同的票数,比如100这张票被卖了两次;

② 不存在的票,比如 0 和 -1 票,是不存在的;

2、分析问题

针对于上面的售票现象,为什么会出现这样的情况呢?

当只有一个窗口售票或多个窗口分别出售自己的票是没有问题的。但是当三个窗口,同时访问共享的资源,就会导致线程不同步,这种问题称为 线程不安全。

线程安全问题的产生的原理:

可以发现多个线程执行的不确定性引起执行结果的不稳定;

多个线程对公共的数据处理,会造成操作的不完整性,会破坏数据。

注意:线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

3、问题的总结

问题出现的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

如何解决:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

二、同步机制

Java 对于多线程的安全问题提供了专业的解决方式:同步机制

针对上面的售票案例简单描述一下同步机制:

当窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,窗口1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。

也就是说某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

同步方式的分析:

1、同步的方式,解决了线程的安全问题(好处)

2、操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。(局限性)

同步的注意项:

(1)操作共享数据的代码,即为需要被同步的代码加锁;

不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

(2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

(3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

要求:多个线程必须要共用同一把锁。

三、方式一:同步代码块

1、语法格式

synchronized (对象){

// 需要被同步的代码 / 可能会出现线程安全问题的代码(访问共享数据的代码)

}

2、实现方式使用同步代码块

代码示例:

1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4

5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8

9 //同时卖票

10 t1.start();11 t2.start();12 t3.start();13 }14 }15

16 class Window implementsRunnable {17

18 private int ticket = 100;19 //创建一个 同步监视器,锁对象

20 private Object obj = newObject();21

22 @Override23 public voidrun() {24

25 while (true) {26 synchronized(obj) { //使用 synchronized 给操作共享数据的地方加锁27 //有票,可以出售

28 if (ticket > 0) {29

30 //出票操作,使用 sleep 模拟一下出票时间

31 try{32 Thread.sleep(100);33 } catch(InterruptedException e) {34 e.printStackTrace();35 }36 //获取当前线程对象的名字

37 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);38

39

40 ticket--;41 } else{42 break;43 }44 }45 }46 }47 }

如果加锁都需要创建一个对象,我们可以改进一下:

1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4

5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8

9 //同时卖票

10 t1.start();11 t2.start();12 t3.start();13 }14 }15

16 class Window implementsRunnable {17

18 private int ticket = 100;19

20 @Override21 public voidrun() {22

23 while (true) {24 synchronized (this) { //方式二:此时的this:唯一的Window1的对象25 //有票,可以出售

26 if (ticket > 0) {27

28 //出票操作,使用 sleep 模拟一下出票时间

29 try{30 Thread.sleep(100);31 } catch(InterruptedException e) {32 e.printStackTrace();33 }34 //获取当前线程对象的名字

35 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);36

37

38 ticket--;39 } else{40 break;41 }42 }43 }44 }45 }

注意:这里的 this指的就是当前的 Window 对象,因为在main方法中多个 Thread 共用了同一个 Window 对象,所以这里的this 是公共的锁。

3、继承方式使用同步代码块

1 public classWindowTest2 {2 public static voidmain(String[] args) {3 Window2 t1 = newWindow2();4 Window2 t2 = newWindow2();5 Window2 t3 = newWindow2();6

7 t1.setName("窗口1");8 t2.setName("窗口2");9 t3.setName("窗口3");10

11 t1.start();12 t2.start();13 t3.start();14 }15 }16

17 class Window2 extendsThread {18 private static int ticket = 100;19 private static Object obj = newObject();20

21 @Override22 public voidrun() {23

24 while (true) {25 synchronized(obj) {26 if (ticket > 0) {27

28 try{29 Thread.sleep(100);30 } catch(InterruptedException e) {31 e.printStackTrace();32 }33

34 System.out.println(getName() + ":卖票,票号为:" +ticket);35 ticket--;36 } else{37 break;38 }39 }40 }41 }42 }

对于继承的方式,如果我们也想避免创建的繁琐,可以这样写:

1 public classWindowTest2 {2 public static voidmain(String[] args) {3 Window2 t1 = newWindow2();4 Window2 t2 = newWindow2();5 Window2 t3 = newWindow2();6

7 t1.setName("窗口1");8 t2.setName("窗口2");9 t3.setName("窗口3");10

11 t1.start();12 t2.start();13 t3.start();14 }15 }16

17 class Window2 extendsThread {18 private static int ticket = 100;19

20 @Override21 public voidrun() {22

23 while (true) {24 synchronized (Window2.class) { //方式2

25 if (ticket > 0) {26

27 try{28 Thread.sleep(100);29 } catch(InterruptedException e) {30 e.printStackTrace();31 }32

33 System.out.println(getName() + ":卖票,票号为:" +ticket);34 ticket--;35 } else{36 break;37 }38 }39 }40 }41 }

注意:这里是不能使用 this 的,因为在 main 中创建了多个 Window2对象,它们各不一样。但是我们可以使用当前类的对象,全局唯一的类对象来充当锁。因为类对象只会加载一次,是全局唯一的。

4、小结

(1)操作共享数据的代码,即为需要被同步的代码加锁;

不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

(2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

(3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

要求:多个线程必须要共用同一把锁。

注意:(具体情况还要具体分析!)

① 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

② 在继承Thread类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。

四、方式二:同步方法

1、语法格式

public static synchronized void show (String name){

可能会产生线程安全问题的代码 / 可能会出现线程安全问题的代码(访问了共享数据的代码)

}

synchronized还可以放在方法声明中,表示整个方法为同步方法。

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

2、实现方式使用同步方法

代码示例:

1 public classWindowTest3 {2 public static voidmain(String[] args) {3 Window3 w = newWindow3();4

5 Thread t1 = newThread(w);6 Thread t2 = newThread(w);7 Thread t3 = newThread(w);8

9 t1.setName("窗口1");10 t2.setName("窗口2");11 t3.setName("窗口3");12

13 t1.start();14 t2.start();15 t3.start();16 }17 }18

19 class Window3 implementsRunnable {20 private int ticket = 100;21

22

23 @Override24 public voidrun() {25 while (true) {26 show();27 }28 }29

30 private synchronized voidshow() { //同步监视器:this31 if (ticket > 0) {32

33 try{34 Thread.sleep(100);35 } catch(InterruptedException e) {36 e.printStackTrace();37 }38

39 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);40

41 ticket--;42 }43 }44 }

注意:这里的锁对象是 this,因为在 main 方法中还是共用了同一个 Window3 对象,这里的 this 就是此对象,可以公共的锁对象。如果在 main 中创建了多个 Window3 对象并传递给 Thread来启动,这样并不可以保证同步哦!

3、继承方式使用同步方法

代码示例:

1 public classWindowTest4 {2 public static voidmain(String[] args) {3 Window4 t1 = newWindow4();4 Window4 t2 = newWindow4();5 Window4 t3 = newWindow4();6

7

8 t1.setName("窗口1");9 t2.setName("窗口2");10 t3.setName("窗口3");11

12 t1.start();13 t2.start();14 t3.start();15

16 }17 }18

19 class Window4 extendsThread {20 private static int ticket = 100;21

22 @Override23 public voidrun() {24 while (true) {25 show();26 }27 }28

29 private static synchronized void show() { //同步监视器:Window4.class

30 if (ticket > 0) {31

32 try{33 Thread.sleep(100);34 } catch(InterruptedException e) {35 e.printStackTrace();36 }37

38 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);39 ticket--;40 }41 }42 }

注意:在继承中这样使用的锁对象就是 Window4.class,当前的类对象。

切记不能写成这样

//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的

这样他们的锁对象就不再是类对象,而是以每个实例对象为锁的,并不是公共的锁,不能保证同步。

4、小结

(1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

(2)非静态的同步方法,同步监视器是实现类对象:this;

静态的同步方法,同步监视器是当前类对象:当前类本身

五、同步的总结

1、分析同步原理

2、同步机制中的锁

(1)同步锁机制

(2)synchronized 的锁是什么?

① 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。

② 同步方法的锁:静态方法(类名.class)、非静态方法(this)

③ 同步代码块:自己指定,很多时候也是指定为 this 或 类名.class

(3)注意

① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则无法保证共享资源的安全;

② 一个线程类中的所有静态方法共用一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

3、同步的范围

(1)如何找问题,即代码是否存在线程安全?(重要)

① 明确哪些代码是多线程运行的代码;

② 明确多个线程是否有共享数据;

③ 明确多线程运行代码中是否有多条语句操作共享数据;

(2)如果解决?(重要)

① 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行;

② 即所有操作共享数据的这些语句都要放在同步范围中;

(3)切记

① 范围太小:没锁住所有有安全问题的代码;

② 范围太大:没发挥多线程的功能;

4、释放锁的操作

(1)当前线程的同步方法、同步代码块执行结束;

(2)当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行;

(3)当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束;

(4)当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。

5、不会释放锁的操作

(1)线程执行同步代码块或同步方式时,程序调用 Thread.sleep()、Thread.yield()方法暂停当前线程的执行;

(2)线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(不同监视器);

应尽量避免使用 suspend() 和 resume() 来控制线程;

六、Lock 锁——JDK5.0 新增锁

1、Lock 锁

(1)从 JDK5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步,同步锁使用 Lock 对象充当;

(2)java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。

锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

(3)ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

2、语法

class A{

private final ReentrantLock lock = new ReenTrantLock();

public void m(){

lock.lock();

try{

//保证线程安全的代码;

}

finally{

lock.unlock();

}

}

}

注意:如果同步代码有异常,要将unlock( )写入finally语句块

3、实现方式使用 Lock 锁

使用步骤:

① 在成员位置创建一个ReentrantLock对象(Lock接口的一个实现类)

② 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁

③ 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

示例:

1 public classWindowTest5 {2 public static voidmain(String[] args) {3 Window5 w = newWindow5();4

5 Thread t1 = newThread(w);6 Thread t2 = newThread(w);7 Thread t3 = newThread(w);8

9 t1.setName("窗口1");10 t2.setName("窗口2");11 t3.setName("窗口3");12

13 t1.start();14 t2.start();15 t3.start();16 }17 }18

19 class Window5 implementsRunnable {20

21 private int ticket = 100;22 //1.实例化ReentrantLock

23 private ReentrantLock lock = newReentrantLock();24

25 @Override26 public voidrun() {27 while (true) {28 try{29 //2.调用锁定方法 lock()

30 lock.lock();31

32 if(ticket > 0){33

34 try{35 Thread.sleep(100);36 } catch(InterruptedException e) {37 e.printStackTrace();38 }39

40 System.out.println(Thread.currentThread().getName() + ":售票,票号为:" +ticket);41 ticket--;42 }else{43 break;44 }45 } finally{46 //3.调用解锁方法:unlock(),无论程序是否异常,都会把锁释放

47 lock.unlock();48 }49 }50 }51 }

4、继承方式使用 Lock 锁

代码示例:

1 public classWindowTest6 {2 public static voidmain(String[] args) {3 Window6 w1 = newWindow6();4 Window6 w2 = newWindow6();5 Window6 w3 = newWindow6();6

7 w1.setName("窗口一");8 w2.setName("窗口二");9 w3.setName("窗口三");10

11 w1.start();12 w2.start();13 w3.start();14 }15 }16

17 class Window6 extendsThread {18

19 private static int ticket = 100;20 //1.实例化ReentrantLock

21 private static ReentrantLock lock = newReentrantLock();22

23 @Override24 public voidrun() {25 while (true) {26 try{27 //2.调用锁定方法 lock()

28 lock.lock();29

30 if(ticket > 0){31

32 try{33 Thread.sleep(100);34 } catch(InterruptedException e) {35 e.printStackTrace();36 }37

38 System.out.println(Thread.currentThread().getName() + ":售票,票号为:" +ticket);39 ticket--;40 }else{41 break;42 }43 } finally{44 //3.调用解锁方法:unlock()

45 lock.unlock();46 }47 }48 }49 }

5、synchronized 与 Lock 的对比

(1)Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用自动释放;

(2)Lock 只有代码块锁,synchronized 有代码块锁和方法所;

(3)使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock ——>同步代码块(已经进入了方法体,分配了相应资源)——>同步方法(在方法体之外)

七、线程的死锁问题

1、死锁

(1)不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;

(2)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;

(3)死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

2、死锁的必要条件

死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

3、解决方法

(1)专门的算法、原则;

(2)尽量减少同步资源的定义;

(3)尽量避免嵌套同步

4、示例

示例一:

1 //死锁的演示

2 classA {3 public synchronized void foo(B b) { //同步监视器:A类的对象:a

4 System.out.println("当前线程名: " +Thread.currentThread().getName()5 + " 进入了A实例的foo方法"); //①

6 try{7 Thread.sleep(200);8 } catch(InterruptedException ex) {9 ex.printStackTrace();10 }11 System.out.println("当前线程名: " +Thread.currentThread().getName()12 + " 企图调用B实例的last方法"); //③

13 b.last();14 }15

16 public synchronized void last() {//同步监视器:A类的对象:a

17 System.out.println("进入了A类的last方法内部");18 }19 }20

21 classB {22 public synchronized void bar(A a) {//同步监视器:b

23 System.out.println("当前线程名: " +Thread.currentThread().getName()24 + " 进入了B实例的bar方法"); //②

25 try{26 Thread.sleep(200);27 } catch(InterruptedException ex) {28 ex.printStackTrace();29 }30 System.out.println("当前线程名: " +Thread.currentThread().getName()31 + " 企图调用A实例的last方法"); //④

32 a.last();33 }34

35 public synchronized void last() {//同步监视器:b

36 System.out.println("进入了B类的last方法内部");37 }38 }39

40 public class DeadLock1 implementsRunnable {41 A a = newA();42 B b = newB();43

44 public voidinit() {45 Thread.currentThread().setName("主线程");46 //调用a对象的foo方法

47 a.foo(b);48 System.out.println("进入了主线程之后");49 }50

51 public voidrun() {52 Thread.currentThread().setName("副线程");53 //调用b对象的bar方法

54 b.bar(a);55 System.out.println("进入了副线程之后");56 }57

58 public static voidmain(String[] args) {59 DeadLock1 dl = newDeadLock1();60 newThread(dl).start();61

62

63 dl.init();64 }65 }

示例二:

1 public classDeadLock2 {2 public static voidmain(String[] args) {3

4 StringBuffer s1 = newStringBuffer();5 StringBuffer s2 = newStringBuffer();6

7

8 newThread() {9 @Override10 public voidrun() {11

12 synchronized(s1) {13

14 s1.append("a");15 s2.append("1");16

17 try{18 Thread.sleep(100);19 } catch(InterruptedException e) {20 e.printStackTrace();21 }22

23

24 synchronized(s2) {25 s1.append("b");26 s2.append("2");27

28 System.out.println(s1);29 System.out.println(s2);30 }31 }32 }33 }.start();34

35

36 new Thread(newRunnable() {37 @Override38 public voidrun() {39 synchronized(s2) {40

41 s1.append("c");42 s2.append("3");43

44 try{45 Thread.sleep(100);46 } catch(InterruptedException e) {47 e.printStackTrace();48 }49

50 synchronized(s1) {51 s1.append("d");52 s2.append("4");53

54 System.out.println(s1);55 System.out.println(s2);56 }57 }58 }59 }).start();60 }61

62 }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值