34 多线程(六)——线程安全 synchronized
一个小总结
Synchronized与同步块的形象比喻:
我们以去商店买衣服为比喻:synchrnized锁方法就好比去一家商店买衣服,一次只能进一个人,买完出来才能进第二个人。而同步块则是在整个买衣服流程的关键之处:试衣间换衣服,结账(假设只有一个试衣间,只有一个收银台)时做了排队处理,排队使得数据不会错乱同步块锁的就是临界资源(试衣间、收银台)。
概念
关键字synchronized可以写在方法和代码块中
- 写在普通方法中:锁住的对象是this,即类的实例。也就是说锁住的是类下面的类变量(成员变量),而不是方法中的变量。
- 写在静态方法中:锁住的对象时class
- 写在代码块中,只锁住代码块中的内容
关于这个synchronized关键字
- 线程锁会造成性能下降
- 线程锁用在大的方法中,很影响性能
关于线程锁
- 除了使用synchronized关键字外,还可以使用另一种线程锁,本文没有收录方法
写在方法声明中:synchronized锁对象
案例1
下面来看一个没有加线程锁的案例:3个线程抢票
package _20191205; /** * 线程不安全: * @author TEDU */ public class SynTest01 { public static void main(String[] args) { //一份资源 SafeWeb12306 web = new SafeWeb12306(); new Thread(web,"线程1").start(); new Thread(web,"线程2").start(); new Thread(web,"线程3").start(); } } class SafeWeb12306 implements Runnable{ //票数 private int ticketNums = 100; private boolean flag = true; @Override public void run() { while(flag) { try{ Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } buy(); } } //买票:线程不安全 public void buy() { if(ticketNums<=0) { flag = false; return; } try { Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } }
它的运行结果:
会出现多个线程抢了同一张票的情况,为了避免这种情况,我们需要给关键方法加锁,在本例中,我们只需要给buy()方法加锁即可,即:
public void synchronized buy(){...}
注意的地方
注意我们只new了一个资源的实例,当三个线程对它的成员变量进行操作时,才能使用synchronized对这个实例进行线程保护,锁住这个实例的成员变量。
SafeWeb12306 web = new SafeWeb12306(); new Thread(web,"线程1").start(); new Thread(web,"线程2").start(); new Thread(web,"线程3").start();
案例2 这个例子就不要看了
两个人都持有同一个账户的银行卡。
现两人同时使用两台ATM机取款,要保证线程安全,就要在增加和减少账户余额的方法中加入sychronized关键字
package _20191205; import java.util.Scanner; /** * synchronized案例 * @author TEDU *模拟两个人从取款机取同一个账户 *人类,取款机类,银行账户类,账户数据库类 *线程锁锁的是账户数据库类,也就是锁在这个类的取款与存款方法 */ public class synTest02 {//测试类 public static void main(String[] args) { DataBase db = new DataBase();//生成一个账户的数据库(相当于一张卡) Man m1 = new Man("小明",db);//两个人都持有这张卡 Man m2 = new Man("小李",db);//两个人都持有这张卡 new Thread(m1,"人1").start();; new Thread(m1,"人2").start(); } } class Man implements Runnable{ private String name; private ATM atm; private DataBase db; public Man(String name,DataBase db) { this.name = name; this.db = db; } //与ATM交互 public void operateATM() { System.out.println(name+"正在操作ATM机,掏出准备好的小纸条:账号111,密码222"); atm = new ATM(db); atm.logIn(); System.out.println(name+"已经操作完ATM机"); } @Override public void run() { // TODO Auto-generated method stub operateATM(); } } class ATM { private DataBase db; public ATM(DataBase db) { this.db = db; } Account account; //查询余额方法 private void inquire() { account.doMath("inquire"); } //取款方法 private void withDraw() {//传入要取的数量 int num; Scanner scan = new Scanner(System.in); System.out.println("请输入取款金额:"); num = scan.nextInt(); account.doMath("sub",num); } //存款方法 private void save() { int num; Scanner scan = new Scanner(System.in); System.out.println("请假装整理好您的钞票,假装放进取款机!"); num = scan.nextInt(); System.out.println("存入中,请稍后......"); account.doMath("add",num); System.out.println("存入成功!"); } public void logIn() {//登入方法 int command;//用于接收用户命令 System.out.println("======欢迎使用口袋银行ATM系统======"); Scanner scan = new Scanner(System.in); System.out.println("请输入账号:"); String acc = scan.nextLine(); System.out.println("请输入密码:"); String psd = scan.nextLine(); account = new Account(acc,psd,db); //判断账户与密码是否存在 if(account.compare()) { //账户密码均正确 System.out.println("登入成功!"); do { System.out.println("请选择您的操作:1.查询余额 2.取款 3.存款 0.退出"); command = scan.nextInt(); switch(command) { case 0: break; case 1: //查询余额方法 inquire(); break; case 2: //调用取款方法 withDraw(); break; case 3: //调用存款方法 save(); break; default: System.out.println("输入错误!"); break; } }while(command!=0); }else { System.out.println("账户或密码错误"); } } } class Account{ private DataBase db; private String accountName; private String passwd; public Account(String accountName,String psd,DataBase db) { this.accountName = accountName; this.passwd = psd; this.db = db; } //判断用户输入的账号密码是否正确 public boolean compare() { if(db.getAccountName().contentEquals(accountName)&&db.getPasswd().contentEquals(passwd)){ return true; }else { return false; } } //账户余额做计算 public void doMath(String act,int num) { if(act.equals("add")) { //账户添加余额 db.addBalance(num); } if(act.equals("sub")) { //账户减少余额 db.subBalance(num); } } public void doMath(String act) { if(act.equals("inquire")) { System.out.println("您的账户余额为:¥"+db.getBalance()); } } } //本数据库只存了一个账户的信息 class DataBase{ private String accountName = "111"; private String passwd = "222"; private int balance = 10000;//账户余额1w //增加余额方法 public synchronized void addBalance(int num) { this.balance += num; } //减少余额 public synchronized void subBalance(int num) { if(num>balance) { System.out.println("余额不足"); }else { this.balance -= num; System.out.println("出钞中,请稍后......"); System.out.println("请尽快取走钞票!"); } } public int getBalance() { return balance; } public String getAccountName() { return accountName; } public String getPasswd() { return passwd; } }
运行结果
小明正在操作ATM机,掏出准备好的小纸条:账号111,密码222 小明正在操作ATM机,掏出准备好的小纸条:账号111,密码222 ======欢迎使用口袋银行ATM系统====== ======欢迎使用口袋银行ATM系统====== 请输入账号: 请输入账号: 111 111 请输入密码: 请输入密码: 222 222 登入成功! 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 登入成功! 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 1 1 您的账户余额为:¥10000 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 您的账户余额为:¥10000 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 2 2 请输入取款金额: 请输入取款金额: 8000 8000 出钞中,请稍后...... 请尽快取走钞票! 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 余额不足 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 1 1 您的账户余额为:¥2000 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出 您的账户余额为:¥2000 请选择您的操作:1.查询余额 2.取款 3.存款 0.退出
写在代码块中:synchronized锁(同步块)案例
synchronized块的优点为:更细致的对需要线程同步的部分进行加锁,优化代码,使性能提高。
格式:
synchronized (obj) {...} //obj为监视器,即要锁住的对象,注意是对象,不是属性
注意:
- 无论是基础数据类型还是引用数据类型,如果它的改变会引起线程不安全,那它们都要加线程锁
- 要锁住的是对象,注意是对象(引用类型),不是属性(基础类型)
- 被锁的对象可以是this(如果有多个被修改的对象时)
- synchronized关键字要与被锁的对象被修改的地方尽可能近
- synchronized块只能锁一个对象,如果需要需要
案例1
1w个并发线程向一个ArrayList中添加自己的线程名,最后看看这个容器的大小是不是1w。
注意:
- 重点在这里,synchronized代码要与被锁的对象尽可能近
- 被锁的对象可以是this
package _20191205; import java.util.List; import java.util.ArrayList; public class SynBlockTest02 { public static void main(String[] args) { List<String> list = new ArrayList<>(); for(int i = 0;i < 10000;i++ ) { new Thread(()->{
synchronized (list) {//重点在这里,synchronized代码要与被锁的对象使用的地方尽可能近 list.add(Thread.currentThread().getName().toString()); } },"线程"+i).start(); } try { Thread.sleep(8000);//这里休眠8s是因为我们for循环里的线程与main线程是并发的,如果不写休眠,可能main就很快把容器的容量输出来了,就得不到正确的结果 }catch(InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); // for(String str : list) { // System.out.println(str); // } } }
案例2 三个线程抢100张票
本例的重点在于,需要保证线程安全的对象时基础数据类型,而且不止一个基础数据类型被修改,则在这个线程安全中obj为this,代表这两个基础数据类型所在的类的对象。
package _20191205; /** * 线程安全:在并发时保证数据的正确性、效率尽可能的高 * synchronized锁方法案例 * @author TEDU * */ public class SynTest01 { public static void main(String[] args) { //一份资源,注意我们只new了一个资源的实例,当三个线程对它进行操作时,才能使用synchronized对他锁住资源线程保护 SafeWeb12306 web = new SafeWeb12306(); new Thread(web,"线程1").start(); new Thread(web,"线程2").start(); new Thread(web,"线程3").start(); } } class SafeWeb12306 implements Runnable{ //票数 private int ticketNums = 100; private boolean flag = true; @Override public void run() { while(flag) { try{ Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } buy2(); } } //买票:线程安全的买 通过synchronized块 public void buy2() {
//这里再写一遍是因为代码优化,没有票就没有必要等了,考虑的是没有票的情况
if(ticketNums<=0) {
flag = false;
return;
}
//必须涵盖所有会被修改的地方,这里会被修改的地方即flag与ticketNums
synchronized(this) {//由于ticketNums是属性是基础数据类型,不是引用类型,所以直接用this,表示ticketNums所在的类
if(ticketNums<=0) { //这里写,是考虑只有一张票的情况(三个线程都读到了这张票)
flag = false;//被修改的地方1
return;
}
try {
Thread.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);//被修改的地方2
}
}
}