在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。
今天我们接着聊聊多线程线程安全的问题,以及解决办法。
实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。
我们先来看一个简单的线程安全问题的例子!
public class DataEntity { private int count = 0; public void addCount(){ count++; } public int getCount(){ return count; }}
public class DataEntity {
private int count = 0;
public void addCount(){
count++;
}
public int getCount(){
return count;
public class MyThread extends Thread { private DataEntity entity; public MyThread(DataEntity entity) { this.entity = entity; } @Override public void run() { for (int j = 0; j < 1000000; j++) { entity.addCount(); } }}
public class MyThread extends Thread {
private DataEntity entity;
public MyThread(DataEntity entity) {
this.entity = entity;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
entity.addCount();
public class MyThreadTest { public static void main(String[] args) { // 初始化数据实体 DataEntity entity = new DataEntity(); //使用多线程编程对数据进行计算 for (int i = 0; i < 10; i++) { MyThread thread = new MyThread(entity); thread.start(); } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result: " + entity.getCount()); }}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化数据实体
DataEntity entity = new DataEntity();
//使用多线程编程对数据进行计算
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread(entity);
thread.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("result: " + entity.getCount());
多次运行结果如下:
第一次运行:result: 9788554第二次运行:result: 9861461第三次运行:result: 6412249...
第一次运行:result: 9788554
第二次运行:result: 9861461
第三次运行:result: 6412249
...
上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。
但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢?
简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果!
我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!
如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:
如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。
因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。
针对多线程编程中,程序运行不安全的问题,Java 提供了synchronized关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。
synchronized
我们对上面示例中的DataEntity.addCount()方法进行改造,再看看效果如下。
DataEntity.addCount()
public class DataEntity { private int count = 0; /** * 在方法上加上 synchronized 关键字 */ public synchronized void addCount(){ count++; } public int getCount(){ return count; }}
/**
* 在方法上加上 synchronized 关键字
*/
public synchronized void addCount(){
第一次运行:result: 10000000第二次运行:result: 10000000第三次运行:result: 10000000...
第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
运行结果与预期一致!
synchronized作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:
synchronized也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。
关于锁的知识,我们后面在介绍,大家先了解一下就行。
从实际的使用角度来看,synchronized修饰的对象有以下几种:
{}
下面我们一起来看看它们的具体用法。
当synchronized修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。
如果多线程访问的不是同一个对象,不会起到保证线程同步的作用。
示例如下:
public class DataEntity { private int count; /** * 在方法上加上 synchronized 关键字 */ public synchronized void addCount(){ for (int i = 0; i < 3; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public int getCount() { return count; }}
private int count;
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
public int getCount() {
public class MyThreadA extends Thread { private DataEntity entity; public MyThreadA(DataEntity entity) { this.entity = entity; } @Override public void run() { entity.addCount(); }}
public class MyThreadA extends Thread {
public MyThreadA(DataEntity entity) {
public class MyThreadB extends Thread { private DataEntity entity; public MyThreadB(DataEntity entity) { this.entity = entity; } @Override public void run() { entity.addCount(); }}
public class MyThreadB extends Thread {
public MyThreadB(DataEntity entity) {
public class MyThreadTest { public static void main(String[] args) { // 初始化数据实体 DataEntity entity = new DataEntity(); MyThreadA threadA = new MyThreadA(entity); threadA.start(); MyThreadB threadB = new MyThreadB(entity); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result: " + entity.getCount()); }}
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
Thread.sleep(1000);
运行结果如下:
Thread-0:0Thread-0:1Thread-0:2Thread-1:3Thread-1:4Thread-1:5result: 6
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。
假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:
public class MyThreadTest { public static void main(String[] args) { DataEntity entity1 = new DataEntity(); MyThreadA threadA = new MyThreadA(entity1); threadA.start(); DataEntity entity2 = new DataEntity(); MyThreadA threadB = new MyThreadA(entity2); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result: " + entity1.getCount()); System.out.println("result: " + entity2.getCount()); }}
DataEntity entity1 = new DataEntity();
MyThreadA threadA = new MyThreadA(entity1);
DataEntity entity2 = new DataEntity();
MyThreadA threadB = new MyThreadA(entity2);
System.out.println("result: " + entity1.getCount());
System.out.println("result: " + entity2.getCount());
Thread-0:0Thread-1:0Thread-0:1Thread-1:1Thread-0:2Thread-1:2result: 3result: 3
Thread-1:0
Thread-1:1
Thread-1:2
result: 3
从结果上可以看出,当synchronized修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。
synchronized修改一个静态的方法时,代表的是对当前.java文件对应的 Class 类加锁,不区分对象实例。
.java
public class DataEntity { private static int count; /** * 在静态方法上加上 synchronized 关键字 */ public synchronized static void addCount(){ for (int i = 0; i < 3; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static int getCount() { return count; }}
private static int count;
* 在静态方法上加上 synchronized 关键字
public synchronized static void addCount(){
public static int getCount() {
public class MyThreadA extends Thread { @Override public void run() { DataEntity.addCount(); }}
DataEntity.addCount();
public class MyThreadB extends Thread { @Override public void run() { DataEntity.addCount(); }}
public class MyThreadTest { public static void main(String[] args) { MyThreadA threadA = new MyThreadA(); threadA.start(); MyThreadB threadB = new MyThreadB(); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result: " + DataEntity.getCount()); }}
MyThreadA threadA = new MyThreadA();
MyThreadB threadB = new MyThreadB();
System.out.println("result: " + DataEntity.getCount());
静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁,类锁可以理解为这个类的所有对象。
synchronized用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。
示例如下
public class DataEntity { private int count; /** * 在方法上加上 synchronized 关键字 */ public void addCount(){ synchronized (this){ for (int i = 0; i < 3; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public int getCount() { return count; }}
synchronized (this){
其中synchronized (this)中的this,表示的是当前类实例的对象,效果等同于public synchronized void addCount()。
synchronized (this)
this
public synchronized void addCount()
除此之外,synchronized()还可以修饰任意实例对象,作用的范围就是具体的实例对象。
synchronized()
比如,修饰个自定义的类实例对象,作用的范围是拥有lock对象,其实也等价于synchronized (this)。
lock
public class DataEntity { private Object lock = new Object(); /** * synchronized 可以修饰任意实例对象 */ public void addCount(){ synchronized (lock){ // todo... } }}
private Object lock = new Object();
* synchronized 可以修饰任意实例对象
synchronized (lock){
// todo...
当然也可以用于修饰类,表示类锁,效果等同于public synchronized static void addCount()。
public synchronized static void addCount()
public class DataEntity { /** * synchronized 可以修饰类,表示类锁 */ public void addCount(){ synchronized (DataEntity.class){ // todo... } }}
* synchronized 可以修饰类,表示类锁
synchronized (DataEntity.class){
synchronized修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
return singleton;
采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。
synchronized关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。
我们看个例子就能明白。
public class DataEntity { private int count = 0; public synchronized void addCount1(){ System.out.println(Thread.currentThread().getName() + ":" + (count++)); addCount2(); } public synchronized void addCount2(){ System.out.println(Thread.currentThread().getName() + ":" + (count++)); addCount3(); } public synchronized void addCount3(){ System.out.println(Thread.currentThread().getName() + ":" + (count++)); } public int getCount() { return count; }}
public synchronized void addCount1(){
addCount2();
public synchronized void addCount2(){
addCount3();
public synchronized void addCount3(){
public class MyThreadA extends Thread { private DataEntity entity; public MyThreadA(DataEntity entity) { this.entity = entity; } @Override public void run() { entity.addCount1(); }}
entity.addCount1();
public class MyThreadB extends Thread { private DataEntity entity; public MyThreadB(DataEntity entity) { this.entity = entity; } @Override public void run() { entity.addCount1(); }}
从结果上看线程没有交替执行,线程Thread-0获取到锁之后,再次调用其它带有synchronized关键字的方法时,可以快速进入,而Thread-1线程需等待对象锁完全释放之后再获取,这就是锁重入。
Thread-0
Thread-1
从上文中我们可以得知,在多线程环境下,恰当的使用synchronized关键字可以保证线程同步,使程序的运行结果与预期一致。
synchronized是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。
关于更深入的原理知识,我们会在 JVM 系列中进行详解。文章内容难免有所遗漏,欢迎网友留言指出。
1、五月的仓颉 - synchronized锁机制
2、你听 - 让你彻底理解Synchronized
作者:程序员志哥 出处:pzblog.cn 资源:微信搜【程序员志哥】关注我,回复 【技术资料】有我准备的一线程序必备计算机书籍、大厂面试资料和免费电子书。 希望可以帮助大家提升技术和能力。
原文链接:https://www.cnblogs.com/dxflqm/p/18022798
本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728