本文共 4790 字,大约阅读时间需要 15 分钟。
“线程安全”是一个老生常谈的话题,但是对线程安全比较规范的定义却不是一件容易的事。
Brian Goetz是这样描述线程安全的:当多个线程同时访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方法进行其它的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是安全的。
简单说:但多个线程同时访问同一个类(对象、方法)的时候,可以得到预期的结果,那么这就是线程安全的。
看一段代码:
public class Xiaoping { //定义一个共享的变量 public static int num = 0; public static void main(String[] args) { //创建三个线程,来进行变量的累加 T1 t1 = new T1(); T1 t2 = new T1(); T1 t3 = new T1(); //启动线程 t1.start(); t2.start(); t3.start(); try { //三个线程结束后,打印结果 t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } //打印结果 System.out.println(Xiaoping.num); }}//自定义一个线程类class T1 extends Thread{ @Override public void run() { for (int i = 0; i < 10000; i++){ Xiaoping.num ++; } }}//分析按照我们的预期,每个线程进行10000的累加,那么三个线程执行结束后就是30000但是:实际结果却有可能不是30000,这就表明该线程是不安全的!!!
引发线程安全问题的主要原因:
1.存在共享数据 2.多线程同时操作共享数据我们既然知道线程安全问题的诱因是1.存在共享数据2.多个线程同时访问共享数据。那么我们就可以从这个思路出发
保证同一时刻只有一个线程去操作共享变量:当一个线程去操作共享数据时,其它的线程必须等待至该线程执行结束后才能执行。这就是我们常说的互斥锁!互斥锁简单说就是互斥访问的锁,它可以做到互斥访问,即一个线程操作共享数据时,其它线程是不能操作共享数据的,只有该线程释放锁之后,其它线程才可以访问共享数据。
Synchronized关键字可以用于解决线程安全。它可以保证同一时刻只有一个线程去执行方法或者代码块(主要是线程中共享变量的操作)。还有一个作用就是可以保证线程变化对其它线程的可见性(共享变量的可见性)完全可以替代volatile关键字
问题?
上述存在线程安全问题的代码怎么修改public static volatile int num = 0;
千万注意,上述的修改是不能保证线程安全的,volatile关键字不能保证线程安全的!!!
我们可以使用synchronized关键字来进行线程安全问题的解决
@Override public synchronized void run() { //使用synchronized关键字来避免线程安全问题 for (int i = 0; i < 10000; i++){ Xiaoping.num ++; } }
1.修饰实例方法,作用于当前实例,调用实例方法时首先获取该实例的锁
2.修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
3.修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
代码分别对synchronized关键字使用的演示
1.synchronized关键字修饰实例方法,这里的实例方法是实例对象的方法,而不是静态方法public class SynchronizedTest { public static void main(String[] args) { Person person = new Person(); Demo d1 = new Demo(person); Demo d2 = new Demo(person); d1.start(); d2.start(); try { d1.join(); d2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(person.num); }}class Demo extends Thread{ private Person person; public Demo(){ } public Demo(Person person){ this.person = person; } @Override public void run() { for(int i = 0; i < 10000; i++){ this.person.add(); } }}class Person { int num = 0; //synchronized修饰的实例方法 public synchronized void add(){ num ++; }}
synchronized修饰实例方法注意事项:
1.多个线程操作的是同一个实例
2.同一个实例的多个实例方法用synchronized关键字修饰public class SynchronizedTest2 { public static void main(String[] args) { T t1 = new T(); T t2 = new T(); T t3 = new T(); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Student.num); }}class Student{ static int num = 0; //使用synchronized关键字修饰静态方法 public static synchronized void add(){ num ++; }}class T extends Thread{ @Override public void run() { for (int i = 0; i < 10000; i++) { Student.add(); } }}
修饰静态方法时,此时的锁对象是Class对象
上述可以这样转换:class Student{ static int num = 0; public static void add(){ synchronized (Student.class){ num ++; } }}
我们知道synchronized关键字可以修饰静态方法和实例方法。但是存在一个问题就是他么都是在执行方法前获取指定对象的锁,那么存在一个问题就是有的方法方法体很大,内容比较多,而需要保证线程安全的代码又比较少,这样是比较浪费性能的,因此我们可以使用同步代码块,来对同步代码快中的内容进行加锁。
在演示代码1的基础上使用同步代码块保证线程安全
public class SynchronizedTest { public static void main(String[] args) { Person person = new Person(); Demo d1 = new Demo(person); Demo d2 = new Demo(person); d1.start(); d2.start(); try { d1.join(); d2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(person.num); }}class Demo extends Thread{ private Person person; public Demo(){ } public Demo(Person person){ this.person = person; } @Override public void run() { for(int i = 0; i < 10000; i++){ this.person.add(); } }}class Person { int num = 0; public void add(){ synchronized (this){ //这里使用的是同步代码快 num ++; } }}
转载地址:http://ojxzi.baihongyu.com/