05-线程同步

 

1.概述
当多个线程在同一时刻访问同一种共享资源时,可能会造成数据的不一致等问题;
为了避免该问题的发生,就需要对线程之间进行协调和通信,而线程之间的协调和通信就是线程同步机制
 
 
一个多线程的程序 如果是通过Runnable 接口来 实现的,则意味着 类中的属性将被多个线程共享,这样一来也会引发一些安全问题。
例如,统计车间工人的数量,经常有人来回出入,很难统计正确。
为解决这样的问题,就需要实现多线程同步,限制某个资源在某一时刻只能被一个线程访问。
 
同步 指的是多个操作在同一时间段内,只能有一个线程进行。其他线程要等待此线程完成之后才可以继续执行。
 
 
2.实现方式(两种)
 
只需要将线程的 并行 改为 串行 就可以解决该问题。
此时可以解决问题,但是效率相对比较低,因此建议能不用则不用。
 
为了协调多个线程的执行,使用java中的 synchronized 关键字,来保证原子性。实现同步锁/对象锁。
 
Java的内部提供了一种 锁机制 来保证 原子性。
原子性:把整个方法的锁定的代码,全部执行完毕。 下一个线程才能执行。
 
(1)同步方法: 使用关键字直接修饰整个方法,锁定整个方法的代码块。
 
权限修饰符 synchronized 返回值类型 方法名([参数1,...]){ 需要同步的代码; }
 
(2)同步代码块: 用该关键字修饰的代码块 被称为 同步代码块。
 
synchronized(同步对象){ 需要同步的代码; }
 
或者:
synchronized(this){ //注意: 同步方法的锁是当前调用该方法的对象,也就是this指向的对象。 希望被锁定的代码块; }
 
原理分析(尽量理解)
当多个线程同时执行同一个方法时,为了避免线程之间的冲突问题,通常都会给该方法加上一把同步锁;
当有线程先抢到同步锁时就可以进入同步语句块执行,其他线程只能进入阻塞状态;
当该线程执行完毕同步语句块后会自动释放同步锁,阻塞的线程又可以抢占同步锁,抢占成功的线程去执行同步语句块,抢占不成功的线程继续阻塞。
 
 
 案例:解决电影院售票产生的 线程安全问题:

 

问题代码:

public class Ticket implements Runnable {               //实现Runnable接口 创建多线程 引发 线程安全问题 
    
    private int tickets=100;
    
    public void run() {
        while(true) {                                          
            try {
                if(tickets>0) {    
                    
                Thread.sleep(10);                                //使线程休眠
            
                String name=Thread.currentThread().getName();   //获取当前线程的名称
                
                System.out.println(name+"正在发售第"+  tickets--  +"张票");  
                
               }else {
                  break;
               }
            } catch (InterruptedException e) {
                
                e.printStackTrace();
            }    
        }
    }
}

测试类

 

public static void main(String[] args) {  
        
        Ticket ticket= new Ticket();               //创建线程任务
        
        Thread t1 = new Thread(ticket,"窗口1");    // 定义4个线程对象,代表4个售票窗口
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");
        Thread t4 = new Thread(ticket,"窗口4");
        
        t1.start();  //开启四个线程
        t2.start();
        t3.start();
        t4.start();
        
         
    }

 

 

结果:

    从运行结果看,不合逻辑    出现负数 ...

 


解决方式一: 使用同步代码块

 

public class Ticket implements Runnable {    //解决线程安全问题,使用 同步代码块(同步锁)
    private int tickets=100;
    public void run() {
while(true) { synchronized(this) { //将this设置为 锁对象 try { if(tickets>0) { Thread.sleep(10); //使线程休眠 String name=Thread.currentThread().getName(); //获取当前线程的名称 System.out.println(name+"正在发售第"+ tickets-- +"张票"); }else { break; } } catch (InterruptedException e) { e.printStackTrace(); } } } } }

测试代码:

public static void main(String[] args) {  
        
        Ticket ticket= new Ticket();//创建线程任务
        
        // 定义4个线程对象,代表4个售票窗口
        Thread t1 = new Thread(ticket,"窗口1");    
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");
        Thread t4 = new Thread(ticket,"窗口4");
        
        //开启四个线程
        t1.start();  
        t2.start();
        t3.start();
        t4.start();
    }

 

问题得到解决:

 


方式二:使用同步方法

 

public class Ticket implements Runnable {    //解决线程安全问题  使用 同步方法             
    private int tickets=100;
    public void run() {
        while(true) {  
            saleTicket();     //调用售票方法
            if(tickets<=0) {
                break;
            }
        }
    }
    private synchronized void saleTicket() {   //定义一个同步方法  saleTicket() 
        if(tickets>0) {
        try {
            if(tickets>0) {     
            Thread.sleep(10);                                //使线程休眠10ms
            String name=Thread.currentThread().getName();   //获取当前线程的名称
            System.out.println(name+"正在发售第"+  tickets--  +"张票");  
           }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }    
     }
    }
}

 

 

测试类:

    public static void main(String[] args) {  
        Ticket ticket= new Ticket();               //创建线程任务
        Thread t1 = new Thread(ticket,"窗口1");    // 定义4个线程对象,代表4个售票窗口
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");
        Thread t4 = new Thread(ticket,"窗口4");
        t1.start();  //开启四个线程
        t2.start();
        t3.start();
        t4.start();
    }
 
 
 
 
 
 
 
 
 
 
 

 
复习:
StringBuilder类 - 后期增加的类,非线程安全的类,效率比较高 【不支持线程的同步技术。】
StringBuffer类 - 早期就有的类,线程安全的类, 效率比较低。 【 支持线程的同步技术。】
 
 
 
3.怎么加锁?
只需要在 重写的run()方法返回值类型前面 加个 关键字synchronized 同步锁,表示将整个方法的所有代码 全部锁定。
当方法体中的代码 全部执行完毕,同步锁会自动打开

 

 

修改后的代码:(即添加了同步锁后)
package com.monkey1030;

public class Account implements Runnable {
    
    private int money = 1000; //账户余额

    @Override
    public synchronized  void run() {                          //   synchronized !!!!!
        
         
            // 模拟ATM机器 去后台服务器读取账户余额的过程
            
            int temp = money;   // 1000
            
            // 判断账户余额是否 >=200
            if(temp >= 200) {
                // 模拟取款过程
                System.out.println("正在出钞,请稍后....");
                temp -= 200;   // 账户余额扣200:  1000 - 200 = 800
                
                try {
                    
                    Thread.sleep(3000);  // 休眠3秒
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走您的钞票!");
                
                
            }else {
                
                System.out.println("账户余额不足,请重新核对金额!");
                
            }
            money = temp;
         

    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

}

 

测试类:

package com.monkey1030;

public class Test {

    public static void main(String[] args) {
        
        Account acc=new Account();
        Thread t1=new Thread(acc);
        Thread t2=new Thread(acc);
        
         
        t1.start();    
        t2.start();
         
        //让主线程 等待子线程终止,然后打印账户余额
        try {
            t1.join();
            t2.join();
             
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        
        System.out.println("账户余额是: "+acc.getMoney());
        
        
        

    }

}

 

结果:

正在出钞,请稍后....
请取走您的钞票!
正在出钞,请稍后....
请取走您的钞票!
账户余额是: 600

 


 注意:

当需要一个静态方法加锁时: “( )”括号中 不再是this
pubic synchronized static void xxx(类名.class){ ............... }
该方法 锁的是对象是 类对象
 
STRINGBUFFER 是同步的 SYNCHRONIZED APPEND();
• STRINGBUILDER 不是同步的 APPEND();
• VECTOR 和 HASHTABLE 是同步的
• ARRAYLIST 和 HASHMAP 不是同步的
• COLLECTIONS.SYNCHRONIZEDLIST()
• COLLECTIONS.SYNCHRONIZEDMAP()
• ARRAYLIST LIST = NEW ARRAYLIST();
• LIST SYNCLIST = COLLECTIONS.SYNCHRONIZEDLIST(LIST);
主要取决于 类中的方法 有无Synchronized 关键字。

 二,死锁的发生

1.概述:
当两个线程或线程者多个互相锁定时就形成了死锁。【使用Synchronized关键字 加锁的时候,要避免死锁的发生。】
 
2. 避免死锁的原则
 
• 顺序上锁,反向解锁,不要回头。
注意:
切记尽量不要使用同步代码块的嵌套!
• JAVA线程的API中很多过时方法都构成了死锁,因此不能调用。
 
 
如:
线程一执行的代码:
 
  public void run(){
        synchronized(a){      - 该线程持有对象锁a,等待对象锁b
           synachronized(b){
              ... ...
           }
        }
    }   

 

线程二执行的代码:

    public void run(){
        synchronized(b){      - 该线程持有对象锁b,等待对象锁a
           synachronized(a){
              ... ...
           }
        }
    }   

 

 

 

 


 

 

 

posted @ 2019-11-02 13:49  小茅棚  阅读(153)  评论(0编辑  收藏  举报