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

      

  • 相关阅读:
    [LeetCode] Search in Rotated Sorted Array II
    [LeetCode] Search in Rotated Sorted Array
    [LeetCode] Rotate List
    [LeetCode] Rotate Array
    [LeetCode] Product of Array Except Self
    [LeetCode] Recover Binary Search Tree
    [LeetCode] Jump Game II
    [LeetCode] Jump Game
    [LeetCode] Delete Node in a Linked List
    [LeetCode] Climbing Stairs
  • 原文地址:https://www.cnblogs.com/Scorpicat/p/11988453.html
Copyright © 2020-2023  润新知