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
		}
	}
}

  

posted @ 2019-12-05 12:02  Scorpicat  阅读(287)  评论(0编辑  收藏  举报