Java多线程——线程之间的同步

Java多线程——线程之间的同步

摘要:本文主要学习多线程之间是如何同步的,以及如何使用synchronized关键字和volatile关键字。

部分内容来自以下博客:

https://www.cnblogs.com/hapjin/p/5492880.html

https://www.cnblogs.com/paddix/p/5367116.html

https://www.cnblogs.com/paddix/p/5428507.html

https://www.cnblogs.com/liuzunli/p/10181869.html

多线程之间的并发问题

在使用多线程的时候,如果多个线程之间有共享的数据,并且其中一个线程在操作共享数据的时候,其他线程也能操作共享数据,那么就有可能引发线程的并发问题。

多售票窗口同时售票引发的并发问题

情景说明:

有2个售票窗口同时售卖3张车票,在这个情境中,用2个线程模拟2个售票窗口,3张车票是共享资源,可售卖的编号是1到3,从3号车票开始售卖。

如果在售票时没有考虑线程的并发问题,2个窗口都能同时修改车票资源,则很容易引发多线程的安全问题。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         Thread t1 = new Thread(dt, "窗口1");
 5         Thread t2 = new Thread(dt, "窗口2");
 6         t1.start();
 7         t2.start();
 8     }
 9 }
10
11 class DemoThread implements Runnable {
12     private int ticket = 3;
13
14     @Override
15     public void run() {
16         while (ticket > 0) {
17             System.out.println(Thread.currentThread().getName() + " 进入卖票环节 ");
18             try {
19                 Thread.sleep(1);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23             System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
24         }
25     }
26 }

运行结果如下:

1 窗口1 进入卖票环节
2 窗口2 进入卖票环节
3 窗口1 售卖的车票编号为: 3
4 窗口2 售卖的车票编号为: 2
5 窗口1 进入卖票环节
6 窗口2 进入卖票环节
7 窗口1 售卖的车票编号为: 1
8 窗口2 售卖的车票编号为: 0

结果说明:

从结果中我们看到窗口1在最后一次售卖中,卖出了编号为0的车票,实际上是不存在的。

出现这种问题的原因是当车票还剩1张的时候,2个窗口同时判断车票数量是否大于1,这时2个窗口就同时进入了售票扣减的代码,导致本来只能卖出1张的车票被2个窗口各自卖出了1张,从而产生了不存在的车票。

在程序里产生这种问题一般都是因为时间片的切换导致的,当一个线程进入操作共享资源的代码块时,时间片用完,另一个线程也通过判断进入了同一个代码块,导致第二个线程在操作共享资源时,没有重新进行判断。也就是说线程对共享资源的操作时不完整的,中间有可能被其他线程对资源进行修改。

产生并发问题的原因

多个线程操作共享的数据。

一个线程在操作共享数据时,其他线程也操作了共享数据。

使用synchronized关键字实现线程之间的同步

synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

synchronized的作用有三个:

◆ 确保线程互斥的访问同步代码。

◆ 保证共享变量的修改能够及时可见。

◆ 有效解决重排序问题。

从语法上讲,synchronized总共有三种用法:

◆ 修饰普通方法。

◆ 修饰静态方法。

◆ 修饰代码块。

接下来我就通过几个例子程序来说明一下这三种使用方式。

使用synchronized的同步代码块

使用synchronized关键字修饰的代码块将对共享资源的操作封装起来,当有一个线程运行代码块时,其他线程只能等待,从而避免共享资源被其他线程修改。

要求多个线程同步使用的锁都必须是同一个才能保证同步,常用的是使用一个Object对象,或者使用this,或者使用类的class对象。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         Thread t1 = new Thread(dt, "窗口1");
 5         Thread t2 = new Thread(dt, "窗口2");
 6         t1.start();
 7         t2.start();
 8     }
 9 }
10
11 class DemoThread implements Runnable {
12     private int ticket = 3;
13
14     @Override
15     public void run() {
16         while (ticket > 0) {
17             try {
18                 Thread.sleep(1);
19             } catch (InterruptedException e) {
20                 e.printStackTrace();
21             }
22             synchronized (DemoThread.class) {
23                 if (ticket > 0) {
24                     System.out.println(Thread.currentThread().getName() + " 进入卖票环节 ");
25                     System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
26                 }
27             }
28         }
29     }
30 }

运行结果如下:

1 窗口1 进入卖票环节
2 窗口1 售卖的车票编号为: 3
3 窗口2 进入卖票环节
4 窗口2 售卖的车票编号为: 2
5 窗口1 进入卖票环节
6 窗口1 售卖的车票编号为: 1

结果说明:

线程在进入卖票的代码块之前,先看一下当前是否由其他线程在执行代码块,如果有其他线程在执行代码块则会等待,直到其他线程执行完之后才能进入代码块,从而保证了线程并发的安全问题。

使用synchronized的普通同步方法

将操作共享资源的代码封装为方法,添加synchronized关键字修饰,这个方法就是同步方法,使用的锁是this对象。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         Thread t1 = new Thread(dt, "窗口1");
 5         Thread t2 = new Thread(dt, "窗口2");
 6         t1.start();
 7         t2.start();
 8     }
 9 }
10
11 class DemoThread implements Runnable {
12     private int ticket = 3;
13
14     @Override
15     public void run() {
16         while (ticket > 0) {
17             try {
18                 Thread.sleep(1);
19             } catch (InterruptedException e) {
20                 e.printStackTrace();
21             }
22             sale();
23         }
24     }
25
26     public synchronized void sale() {
27         if (ticket > 0) {
28             System.out.println(Thread.currentThread().getName() + " 进入卖票环节 ");
29             System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
30         }
31     }
32 }

运行结果如下:

1 窗口1 进入卖票环节
2 窗口1 售卖的车票编号为: 3
3 窗口2 进入卖票环节
4 窗口2 售卖的车票编号为: 2
5 窗口2 进入卖票环节
6 窗口2 售卖的车票编号为: 1

结果说明:

在每次调用sale()方法售票的时候,程序会将实例对象this作为锁,保证一个时间只能有一个线程在操作共享资源。

使用synchronized的静态同步方法

如果该方法是静态方法,因为静态方法优先于类的实例化,所以静态方法是不能持有this的,静态同步方法的琐是类的class对象。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         Thread t1 = new Thread(dt, "窗口1");
 5         Thread t2 = new Thread(dt, "窗口2");
 6         t1.start();
 7         t2.start();
 8     }
 9 }
10
11 class DemoThread implements Runnable {
12     private static int ticket = 3;
13
14     @Override
15     public void run() {
16         while (ticket > 0) {
17             try {
18                 Thread.sleep(1);
19             } catch (InterruptedException e) {
20                 e.printStackTrace();
21             }
22             sale();
23         }
24     }
25
26     public static synchronized void sale() {
27         if (ticket > 0) {
28             System.out.println(Thread.currentThread().getName() + " 进入卖票环节 ");
29             System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
30         }
31     }
32 }

运行结果如下:

1 窗口2 进入卖票环节
2 窗口2 售卖的车票编号为: 3
3 窗口1 进入卖票环节
4 窗口1 售卖的车票编号为: 2
5 窗口2 进入卖票环节
6 窗口2 售卖的车票编号为: 1

结果说明:

使用静态同步方法除了需要注意共享资源也要用static修饰外,其他的和普通同步方法是一样的。

使用volatile关键字实现资源的可见性

可见性

要想理解volatile关键字,得先了解下JAVA的内存模型:

每个线程都有一个自己的本地内存空间,线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作。

对该变量操作完后,在某个时间再把变量刷新回主内存。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         try {
 4             DemoThread thread = new DemoThread();
 5             thread.start();
 6             Thread.sleep(100);
 7             thread.setRunning(false);
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         }
11     }
12 }
13
14 class DemoThread extends Thread {
15     private boolean isRunning = true;
16
17     public void setRunning(boolean isRunning) {
18         this.isRunning = isRunning;
19     }
20
21     @Override
22     public void run() {
23         System.out.println("进入方法");
24         while (isRunning) {
25         }
26         System.out.println("执行完毕");
27     }
28 }

运行结果如下:

1 进入方法

结果说明:

线程一直在运行,并没有因为调用了setRunning()方法就停止了运行。

现在有两个线程,一个是main线程,另一个是RunThread。它们都试图修改isRunning变量。按照JVM内存模型,main线程将isRunning读取到本地线程内存空间,修改后,再刷新回主内存。

而在JVM设置成-server模式运行程序时,线程会一直在私有堆栈中读取isRunning变量。因此,RunThread线程无法读到main线程改变的isRunning变量,从而出现了死循环,导致RunThread无法终止。

解决办法就是在isRunning变量上加上volatile关键字修饰,它强制线程从主内存中取volatile修饰的变量。

代码如下:

1 private volatile boolean isRunning = true;

运行结果如下:

1 进入方法
2 执行完毕

有序性

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:

◆ 重排序操作不会对存在数据依赖关系的操作进行重排序。

比如: a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

◆ 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

比如: a=1;b=2;c=a+b; 这三个操作,第一步 a=1; 和第二步 b=2; 由于不存在数据依赖关系,所以可能会发生重排序,但是 c=a+b; 这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了。

但是运行代码并不能找到支持指令重排序的结果,所以这个地方以后还需要补充。

代码如下:

 1 public class Demo {
 2     private int count = 1;
 3     private boolean flag = false;
 4
 5     public void write() {
 6         count = 2;
 7         flag = true;
 8     }
 9
10     public void read() {
11         if (flag) {
12             System.out.print(count);
13         }
14     }
15
16     public static void main(String[] args) {
17         for (int i = 0; i < 100; i++) {
18             Demo demo = new Demo();
19             Thread write = new Thread(() -> {
20                 demo.write();
21             });
22             Thread read = new Thread(() -> {
23                 demo.read();
24             });
25             write.start();
26             read.start();
27         }
28     }
29 }

预测结果说明:

控制台打印的数据中应该有1出现,但实际情况却只以后2,这个并不能看出程序作了重排序。

预测有1出现的原因是,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而在write()方法中由于第一步 count = 2; 和第二步 flag = true; 不存在数据依赖关系,有可能会被重排序。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

原子性

所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。

volatile只能保证对单次读/写的原子性,不能保证复合类操作的原子性。

代码如下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         Thread[] threads = new Thread[10];
 5         for (int i = 0; i < 10; i++) {
 6             threads[i] = new Thread(demoThread);
 7             threads[i].start();
 8         }
 9         try {
10             Thread.sleep(1000);
11             System.out.println(demoThread.count);
12         } catch (InterruptedException e) {
13             e.printStackTrace();
14         }
15     }
16 }
17
18 class DemoThread extends Thread {
19     public volatile int count = 0;
20
21     @Override
22     public void run() {
23         try {
24             Thread.sleep(1);
25         } catch (InterruptedException e) {
26             e.printStackTrace();
27         }
28         add();
29     }
30
31     private void add() {
32         for (int i = 0; i < 100; i++) {
33             count++;
34         }
35     }
36 }

运行结果如下:

1 986

结果说明:

在多线程环境下,有可能一个线程将count读取到本地内存中,此时其他线程可能已经将count增大了很多,线程依然对过期的count进行自加,重新写到主存中,最终导致了count的结果不合预期,而是小于1000。

如果想要在复合类的操作中保证原子性,可用使用synchronized关键字来实现,还可以通过Java并发包中的循环CAS的方式来保证。

synchronized关键字和volatile关键字的区别

含义

volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。

synchronized主要通过对象锁控制线程对共享数据的访问,持有相同对象锁的线程只能等其他持有同一个对象锁的线程执行完毕之后,才能持有这个对象锁访问和处理共享数据。

比较

◆ 量级比较

volatile轻量级,只能修饰变量。

synchronized重量级,还可修饰方法。

◆ 可见性和原子性

volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

原文地址:https://www.cnblogs.com/shamao/p/10836338.html

时间: 2024-08-24 22:24:43

Java多线程——线程之间的同步的相关文章

java 多线程—— 线程等待与唤醒

java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 java 多线程—— 线程等待与唤醒 概述 第1部分 wait(), notify(), notifyAll()等方法介绍 第2部分 wait()和notify()示例 第3部分 wait(long timeout)和notify() 第4部分 wait() 和 notifyAll() 第5部分 

java 多线程—— 线程让步

java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 java 多线程—— 线程等待与唤醒 java 多线程—— 线程让步 概述 第1 部分 yield()介绍 第2 部分 yield()示例 第3 部分 yield() 与 wait()的比较 第1 部分 yield()介绍 yield()的作用是让步.它能让当前线程由“运行状态”进入到“就绪状态”

C# 线程之间的同步

1.通过Join方法,暂停当前线程 Thread secondThread = new Thread(new ThreadStart(ThreadMethod)); secondThread.Start(); ... secondThread.Join(); 2.通过启动APM异步操作的方法,得到一个IAsyncResult对象,通过它有三种方法使得两个线程同步. public interface IAsynResult { object AsyncState{get; } WaitHandle

Java多线程——线程同步

在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系.可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题.现在就来学习多线程对数据访问的控制吧. 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问.   一.多线程引起的数据访问安全问题 下面看一个经典的问题,银行取钱的问题: 1).你有一张银行卡,里面有50

JAVA多线程--线程的同步安全

每当我们在项目中使用多线程的时候,我们就不得不考虑线程的安全问题,而与线程安全直接挂钩的就是线程的同步问题.而在java的多线程中,用来保证多线程的同步安全性的主要有三种方法:同步代码块,同步方法和同步锁.下面就一起来看: 一.引言 最经典的线程问题:去银行存钱和取钱的问题,现在又甲乙两个人去同一个账户中取款,每人取出800,但是账户中一共有1000元,从逻辑上来讲,如果甲取走800,那么乙一定取不出来800: 1 package thread.threadInBank; 2 3 /** 4 *

Java多线程——线程的生命周期和状态控制

一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常. 2.就绪状态 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它

java 多线程—线程怎么来的

并发处理的广泛应用是使得amdah1定律代替摩尔定律成为计算机性能发展源动力的根本原因,是人类压榨计算机运算能力的最有力武器. 并发并非一定得用多线程,多进程也可以,不过java里面谈论并发,大多数与线程脱不开关系. 1.线程的实现 线程是比进程更轻量级的调度执行单位,在linux里面,线程和进程没有什么区别,唯一的就是在地址空间,线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的. 目前主流的操作系统都提供的线程实现,java则提供的线程实现方法都是native的,因为不同的硬件和操作系

java多线程 - 线程通信

当线程在系统内运行时,程序通常无法准确控制线程的轮换执行,但是可以通过一些机制来保证线程协调运行. 由同步监视器对象协调线程 实现这种功能可以借助于Object类提供的wait().notify().notifyAll()三个方法(注意,这三个方法属于Object类,不属于Thread类).这三个方法必须由同步监视器来调用,可以分为两种情况: 对于同步方法,同步监视器默认是当前实例(this),所以可以在同步方法中直接调用这三个方法: 对于同步代码块,同步监视器是synchronized后括号里

Java多线程——线程阻塞工具类LockSupport

简述 LockSupport 是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞. 和 Thread.suspend()相比,它弥补了由于 resume()在前发生,导致线程无法继续执行的情况. 和 Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出 InterruptedException 异常. LockSupport 的静态方法 park()可以阻塞当前线程,类似的还有 parkNanos().parkUntil()等方法.它们实现了一个限时等待,如下图