多线程的同步-sychronized
线程同步场景
假设盖伦有10000基础血量,这个时候他在基地被别人虐泉水。这时候就会出现这种场景,有多个线程在打击盖伦,减少他的血量。于此同时,基地又有多个线程在给盖伦恢复血量。假设增加血量的线程数和攻击减少血量的线程数是一样的,并且每次改变的值都是1,那么最终盖伦血量应该为基数10000才对。但是,结果不是这个样子的!
示例代码 :Hero
package com.thread; public class Hero { public String name; public float hp; public int damage; //回血 public void revoer() { hp = hp +1; } //掉血 public void hurt() { hp = hp -1; } public void attackHero(Hero h) { try { //为了表示攻击需要时间 每次攻击暂停1秒 Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } h.hp -= damage; System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp); if(h.isDead()) { System.out.println(h.name + "死了!"); } } //判断英雄死了没 public boolean isDead() { return 0 >= hp?true:false; //血量大于0 没死 isDead=false } }
线程同步代码
package com.thread.thread9; import com.thread.Hero; public class TestThread { public static void main(String[] args) { final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; System.out.printf("盖伦初始血量是 %.0f%n", gareen.hp); //多线成同步问题指的是多个线程同时修改一个数据的时候 导致的问题 //假设盖伦有10000滴血 并且在基地里 同时又被多个英雄攻击 //用java代码来表示 就是多个线程在减少盖伦的hp //n个线程增加盖伦的hp int n = 10000; Thread[] addThreads = new Thread[n]; //增加血量 Thread[] reduceThreads = new Thread[n]; //减少血量 for(int i=0; i<n; i++) { Thread t = new Thread() { public void run() { gareen.revoer(); //加血 try { Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } } }; t.start(); //启动线程 addThreads[i] = t; //将单个线程放入线程数组中 } //n个线程减少盖伦的hp for循环内部都是用的局部变量 for(int i=0; i<n; i++) { Thread t = new Thread() { public void run() { gareen.hurt(); try { Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } } }; t.start(); //启动每一个线程 reduceThreads[i] = t; //将当个线程放入减少的线程组中 } //等待所有增加线程结束 for (Thread t: addThreads) { try{ t.join(); }catch (InterruptedException e) { e.printStackTrace(); } } //等待所有减少线程结束 for(Thread t: reduceThreads) { try{ t.join(); }catch(InterruptedException e) { e.printStackTrace(); } } //代码执行到这里 所有增加减少线程都结束了 //增加和减少线程的数量是一样的 每次都是增加 减少1 //那么所有线程都结束后 盖伦的hp应该还是初始值 //但是事实观察到的是 //%d 代表整数参数 //%n 换行 //%.0f float精度 System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了%.0f%n", n, n, gareen.hp); } }
为什么?
理论上我们想要:数据10000+1=10001,然后10001-1=10000
增加线程的操作步骤有三步,取数据,加法,放回数据。减少线程的操作步骤也有三步,取数据,减法,放回数据。
由于线程的启动是很迅速的,而取数据,加法,放回数据这三步需要很长的时间,所以,可能会出现这种情况。
当增加线程刚刚在修改数据的时候,还没有提交数据。减少线程就启动线程了,并且拿到还没增加的10000,进行减一操作,然后再把改好的9999放回去,这样我们最终读到的数据就是9999。
最终读到的9999和理论10000数据不一致,根本原因是两个线程的争抢,一个线程读到了另一个线程还未提交的数据-脏数据。
解决方案-上锁
给每个线程sychronized上锁,当t1线程执行的过程中就把t2线程挡在外面。在每次一个线程执行完成后,重新抢夺cpu资源进行单个线程执行。
线程内部上锁
记录下每行代码执行的时间。给每个线程上锁,看线程占用对象资源后的执行情况。
package com.thread.thread10; import java.text.SimpleDateFormat; import java.util.Date; public class TestThread { public static String now() { //显示当前时间 return new SimpleDateFormat("HH:mm:ss").format(new Date()); //格式化时间 } public static void main(String[] args) { final Object someObject = new Object(); //创建一个所有对象的老祖宗 Thread t1 = new Thread() { //实例一个对象 public void run() { //重写run方法 try { //执行一行代码 都打印时间 System.out.println(now() + "t1线程已经运行"); System.out.println(now() + this.getName() + "试图占有对象: someobject"); synchronized (someObject) { //当前这个线程 占领这个对象 然后给这个对象上锁 System.out.println(now() + this.getName() + "占有对象:someObject"); Thread.sleep(5000); //模拟对象在干事情 System.out.println(now() + this.getName() + "释放对象:someObject"); } System.out.println(now() + "t1 线程结束"); }catch(InterruptedException e) { e.printStackTrace(); } } }; t1.setName("t1"); t1.start(); Thread t2 = new Thread() { public void run() { try{ System.out.println(now() + "t2线程已经运行"); System.out.println(now() + this.getName() + "试图占有对象:someObject"); synchronized (someObject) { //线程t2占领了someobject对象 上了锁 然后其他对象都访问不到了 System.out.println(now() + this.getName() + "占有对象:someobject"); Thread.sleep(5000); System.out.println(now() + this.getName() + "释放对象:someObject"); } System.out.println(now() + "t2线程结束"); }catch (InterruptedException e) { e.printStackTrace(); } } }; t2.setName(" t2"); t2.start(); } }
t1和t2,由于线程启动的时间很短,如果运行程序多次,你会看到t1和t2是没有顺序的。最开始的时候,实际上是他们在抢占资源,谁抢到谁就去占坑位。由于加上了sychronized关键字,所以必须等锁内的最后一行代码,释放了对象,t2才能继续抢占。
synchronized使用方式
分为三种。一是,在线程内部,run方法内的业务逻辑加上锁。二是,在业务方法内部进行加锁,如果有线程使用该方法,就会触发上锁机制。三是,在业务方法上增加锁,如果有线程使用该方法,就会触发上锁机制。
注意: 如果要保证当前线程不会被抢占资源,那么当前线程最好都加上锁,这样才能保证每个线程执行了自己内部的所有任务。
一是,在线程内部上锁,在线程同步代码基础上进行更改。实例线程的时候,内部重写run方法,方法内部业务逻辑加锁。
package com.thread.thread11; import com.thread.Hero; /** * 线程内部每个线程需要执行的方法 给它加上synchronized关键字 使用全局 * object对象 保持全局唯一 */ public class TestThread { public static void main(String[] args) { final Object someObject = new Object(); //声明一个公共的object对象 final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; System.out.printf("盖伦初始血量是 %.0f%n", gareen.hp); //多线成同步问题指的是多个线程同时修改一个数据的时候 导致的问题 //假设盖伦有10000滴血 并且在基地里 同时又被多个英雄攻击 //用java代码来表示 就是多个线程在减少盖伦的hp //n个线程增加盖伦的hp int n = 10000; Thread[] addThreads = new Thread[n]; //增加血量 Thread[] reduceThreads = new Thread[n]; //减少血量 for(int i=0; i<n; i++) { Thread t = new Thread() { public void run() { //在线程内部加血方法加锁 synchronized (someObject) { gareen.revoer(); //加血 } try { Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } } }; t.start(); //启动线程 addThreads[i] = t; //将单个线程放入线程数组中 } //n个线程减少盖伦的hp for循环内部都是用的局部变量 for(int i=0; i<n; i++) { Thread t = new Thread() { public void run() { synchronized (someObject) { gareen.hurt(); } // gareen.hurt(); try { Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } } }; t.start(); //启动每一个线程 reduceThreads[i] = t; //将当个线程放入减少的线程组中 } //等待所有增加线程结束 for (Thread t: addThreads) { try{ t.join(); }catch (InterruptedException e) { e.printStackTrace(); } } //等待所有减少线程结束 for(Thread t: reduceThreads) { try{ t.join(); }catch(InterruptedException e) { e.printStackTrace(); } } //代码执行到这里 所有增加减少线程都结束了 //增加和减少线程的数量是一样的 每次都是增加 减少1 //那么所有线程都结束后 盖伦的hp应该还是初始值 //但是事实观察到的是 //%d 代表整数参数 //%n 换行 //%.0f float精度 System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了%.0f%n", n, n, gareen.hp); } }
二是,在业务方法内部添加锁。
package com.thread.thread12; public class Hero { public String name; public float hp; public int damage; //回血 public void revoer() { hp = hp +1; } //掉血 public void hurt() { //使用this作为同步对象 //哪一个调用这个方法 this就是指代的那一个对象 synchronized (this) { hp = hp -1; } } public void attackHero(Hero h) { try { //为了表示攻击需要时间 每次攻击暂停1秒 Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } h.hp -= damage; //血量在被攻击力一点点的减少 //%s 字符串 %.0f 双精度数 可以带小数 System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp); if(h.isDead()) { System.out.println(h.name + "死了!"); } } //判断英雄死了没 public boolean isDead() { return 0 >= hp?true:false; //血量大于0 没死 isDead=false } }
三是业务方法上添加锁。
package com.thread.thread13; public class Hero { public String name; public float hp; public int damage; //回血 public synchronized void revoer() { hp = hp +1; } //掉血 public synchronized void hurt() { //使用this作为同步对象 //哪一个调用这个方法 this就是指代的那一个对象 hp = hp -1; } public void attackHero(Hero h) { try { //为了表示攻击需要时间 每次攻击暂停1秒 Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } h.hp -= damage; //血量在被攻击力一点点的减少 //%s 字符串 %.0f 双精度数 可以带小数 System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp); if(h.isDead()) { System.out.println(h.name + "死了!"); } } //判断英雄死了没 public boolean isDead() { return 0 >= hp?true:false; //血量大于0 没死 isDead=false } }
线程安全类
如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类。
synchronized意义:同一时间,只有一个线程能够进入这种类的一个实例去修改数据,进而保证这个实例中的数据的安全(不会同时被多线程抢占修改而变成脏数据)
StringBuffer和StringBuilder对比
StringBuffer源码,这个类在多个线程同时修改一个字符串的时候,不会发生字符串拼接异常,它会等待一个拼接成功后,基于上一个线程执行下一段拼接。线程安全
正因为线程安全需要同步,所以效率上比线程不安全的StringBuilder耗时要久,效率要慢。
HashMap和Hashtable对比
Hashtable方法具有synchronized修饰,线程安全。但是不可以放null值。
HashMap线程不安全,可以放null值
ArrayList和Vector对比
vector是线程安全的类,方法上加了synchronized关键字。
线程不安全转成线程安全
这里ArrayList集合创建的list1线程不安全,使用synchronizedList转换成了list2线程安全的对象
List<String> list1 = new ArrayList<>(); List<String> list2 = Collections.synchronizedList(list1);
synchronizedList在Collections中声明了方法,从图中我们可以看到方法内new了两个类SynchronizedRandomAccessList和SynchronizedList
根据源码可以看到,两个类最终都会到一个方法
下图是SynchronizedCollection的声明方法。所以说,list经过synchronizedList方法的转化,相当于在原来list的基础上,封装了一层,每个方法重写并且加了一把锁在上面。