线程安全、线程同步(同步代码块、同步方法、同步锁)

一. 线程安全

1.1 线程安全问题是什么,发生的原因

线程安全、线程同步(同步代码块、同步方法、同步锁)_第1张图片

  • 多个线程同时修改同一共享资源的时候,会出现线程安全问题。
  • 读数据是绝对不会出现线程安全问题的,它一定是因为同时在修改。
  • 一旦线程同步了,就是解决了安全问题了。
  • CPU负责调度线程执行的,它是控制中心。

线程安全、线程同步(同步代码块、同步方法、同步锁)_第2张图片

  1. 线程安全问题出现的原因?

  • 存在多线程并发
  • 同时访问并存在修改同一共享资源

1.2 线程安全问题案例模拟

线程安全、线程同步(同步代码块、同步方法、同步锁)_第3张图片

package com.gch.d3_thread_safe;

/**
   定义账户类
 */
public class Account {
    private double money; // 账户的余额

    public Account(double money) {
        this.money = money;
    }

    public Account() {
    }

    public double getMoney() {
        return money;
    }

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

    /**
     * 取钱功能
     * @param money:取钱的金额
     */
    public void drawMoney(double money){
        // 1.先获取是谁来取钱,线程的名字设置的是人名
        String name = Thread.currentThread().getName();
        // 2.判单账户的余额 >= 取钱的金额
        if(this.money >= money){
            // 可以取钱了
            System.out.println(name + "取钱成功,取出" + money + "元!");
//            setMoney(getMoney() - money);
            // 更新余额
            this.money -= money;
            System.out.println(name + "取钱后共享账户剩余:" + this.money);
        }else{
            System.out.println(name + "来取钱,账户余额不足");
        }
    }
}
package com.gch.d3_thread_safe;

/**
   取钱的线程类
 */
public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;

    /**
     * 有参构造器
     * @param acc:接共享的账户对象
     * @param name:线程名
     */
    public DrawThread(Account acc,String name) {
        super(name);
        this.acc = acc;
    }

    public DrawThread() {
    }

    public Account getAcc() {
        return acc;
    }

    public void setAcc(Account acc) {
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明、小红:取钱
        acc.drawMoney(100000);
    }
}
package com.gch.d3_thread_safe;

/**
   需求:模拟取钱案例
 */
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
        Account acc = new Account(100000);

        // 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
        // 直接new对象这叫匿名对象
        new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);
        new DrawThread(acc,"小红").start();
    }
}

线程安全、线程同步(同步代码块、同步方法、同步锁)_第4张图片

 1. 线程安全问题发生的原因是什么?

  • 多个线程同时访问同一共享资源且存在修改该资源。

 线程安全问题模拟案例二:卖票

  • 案例需求

    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

package com.gch.d3_thread_safe;

public class MyThread extends Thread {
    /**
     * 调用父类的有参构造器
     * @param name:线程名
     */
    public MyThread(String name){
        super(name);
    }
    public static int ticket = 1; // 1 ~ 100
    @Override
    public void run() {
            while(true){
                if(ticket > 100){
                    // 卖完了
                    break;
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                    ticket++;
                }
            }
    }
}
package com.gch.d3_thread_safe;

public class ThreadDemo2 {
    public static void main(String[] args) {
//        需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
//        请设计一个程序模拟该电影院卖票

        // 1.创建线程对象
        Thread t1 = new MyThread("窗口1");
        Thread t2 = new MyThread("窗口2");
        Thread t3 = new MyThread("窗口3");

        // 2.开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}
  •  出现重复票的根本原因是:线程在执行的时候,它是具有随机性的,CPU的执行权有可能随时会被其他的线程给抢走,还没来得及去打印,CPU的执行权就被其他的线程给抢走了。
  • 线程在执行的时候,它是具有随机性的,CPU的执行权随时有可能会被其他的线程给抢走!

线程安全、线程同步(同步代码块、同步方法、同步锁)_第5张图片

 

 二. 线程同步

2.1 同步思想概述

线程同步

  • 为了解决线程安全问题。

1. 取钱案例出现问题的原因?

  • 多个线程同时执行,发现账户都是够钱的。

2. 如何才能保证线程安全呢?

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 把操作共享数据的代码给锁起来!

线程同步的核心思想

  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。其他线程就算抢夺到了CPU的执行权,它也得在外面等着,它进不来!让所有的线程在核心代码当中能轮流执行!
  • 注意:synchronized的锁对象,它一定要是唯一的!如果锁对象不唯一,导致一个线程进一个锁,那么这个锁就没有意义了!

线程同步解决安全问题的思想是什么?

  • 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

 线程安全、线程同步(同步代码块、同步方法、同步锁)_第6张图片

线程安全、线程同步(同步代码块、同步方法、同步锁)_第7张图片

线程安全、线程同步(同步代码块、同步方法、同步锁)_第8张图片

2.2 方式一:同步代码块:

                                       利用同步代码块把操作共享数据的代码给锁起来,让同步代码块里面的代码是轮流去执行的!

线程安全、线程同步(同步代码块、同步方法、同步锁)_第9张图片

锁对象的两个特点:

  • 特点1:锁默认打开,有一个线程进去了,锁自动关闭
  • 特点2:里面的代码全部执行完毕,线程出来,锁自动打开

 锁对象用任意唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的规范要求

  • 规范上:建议使用共享资源作为锁对象。
  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
package com.gch.d5_thread_synchronized_code;

/**
   定义账户类
 */
public class Account {
    private double money; // 账户的余额

    public Account(double money) {
        this.money = money;
    }

    public Account() {
    }

    public double getMoney() {
        return money;
    }

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

    /**
     * 取钱功能
     * @param money:取钱的金额
     */
    public void drawMoney(double money){
        // 1.先获取是谁来取钱,线程的名字设置的是人名
        String name = Thread.currentThread().getName();
        // 同步代码块
        // 小明,小红  唯一的同步锁对象
        // 规范上,建议使用共享资源作为锁对象   this = acc 共享账户
        // 对于实例方法建议使用this作为锁对象
        synchronized (this) {  //  acc.drawMoney(100000);
            // 2.判单账户的余额 >= 取钱的金额
            if(this.money >= money){
                // 可以取钱了
                System.out.println(name + "取钱成功,取出" + money + "元!");
    //            setMoney(getMoney() - money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后共享账户剩余:" + this.money);
            }else{
                System.out.println(name + "来取钱,账户余额不足");
            }
        }
    }
}
package com.gch.d5_thread_synchronized_code;
/**
   取钱的线程类
 */
public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;

    /**
     * 有参构造器
     * @param acc:接共享的账户对象
     * @param name:线程名
     */
    public DrawThread(Account acc, String name) {
        super(name);
        this.acc = acc;
    }

    public DrawThread() {
    }

    public Account getAcc() {
        return acc;
    }

    public void setAcc(Account acc) {
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明、小红:取钱
        acc.drawMoney(100000);
    }
}
package com.gch.d5_thread_synchronized_code;

/**
   需求:模拟取钱案例
 */
public class ThreadSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 测试线程安全问题
        // 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
      Account acc = new Account(100000);

        // 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
        // 直接new对象这叫匿名对象
        new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);
        new DrawThread(acc,"小红").start();
    }
}

 线程安全、线程同步(同步代码块、同步方法、同步锁)_第10张图片

  1. 同步代码块是如何实现线程安全的?
  • 对出现问题的核心代码使用synchronized进行加锁
  • 每次只能一个进程占锁进行访问
  1. 同步代码块的同步锁对象有什么要求?
  • 对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用(当前类的字节码文件对象)字节码(类名.class)对象作为锁对象,字节码文件对象一定是唯一的!

 卖票案例加同步代码块!

 注意:不要把synchronized放在死循环的外面,这样导致一个线程进来以后一直是当前线程在卖票,直到这个线程窗口卖完票才退出,导致其他窗口没有机会!

package com.gch.d3_thread_safe;

public class MyThread extends Thread {
    /**
     * 调用父类的有参构造器
     * @param name:线程名
     */
    public MyThread(String name){
        super(name);
    }
    // 表示这个类所有的对象,都共享ticket数据
    public static int ticket = 1; // 1 ~ 100
    // 锁对象,一定要是唯一的
//    public static Object obj = new Object();
    @Override
    public void run() {
            while(true){
                // 同步代码块  锁对象用当前类的字节码文件,当前类的字节码文件对象一定是唯一的!
                synchronized (MyThread.class) {
                    if(ticket > 100){
                        // 卖完了
                        break;
                    }else{
                        try {
                            Thread.sleep(100);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        System.out.println(getName() + "正在卖第" + ticket + "张票!");
                        ticket++;
                    }
                }
            }
    }
}
package com.gch.d3_thread_safe;

public class ThreadDemo2 {
    public static void main(String[] args) {
//        需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
//        请设计一个程序模拟该电影院卖票

        // 1.创建线程对象
        Thread t1 = new MyThread("窗口1");
        Thread t2 = new MyThread("窗口2");
        Thread t3 = new MyThread("窗口3");

        // 2.开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

 线程安全、线程同步(同步代码块、同步方法、同步锁)_第11张图片

2.3 方式二:同步方法

同步方法

  • 作用:把出现线程安全问题的核心方法上锁
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
  • 线程安全、线程同步(同步代码块、同步方法、同步锁)_第12张图片

同步方法的两个特点:

  • 特点1:同步方法是锁住方法里面所有的代码
  • 特点2:同步方法的锁对象不能自己指定,是Java已经规定好的。如果当前方法是非静态的,那么锁对象就是this,也就是当前方法的调用者;如果当前方法是静态的,那么锁对象是当前类的字节码文件对象

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的(只是我们看不到而已),只是锁的范围是整个方法代码块。
  • 如果方法是实例方法:同步方法默认使用this作为锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认使用类名.class作为锁对象。

同步代码块好还是同步方法好一点儿?

  • 同步代码块锁的范围更小,锁的范围小,性能更好一点儿,而同步方法锁的范围更大。
  • 比如上厕所,一个是在坑位门口锁,一个是在厕所门口锁
  • 但是在实际开发中,同步方法比同步代码块用的更多一点,因为同步方法它的可读性好,写法方便。
  • 官方(JDK)的源码也在大量使用同步方法,比如HashTable。
package com.gch.d6_thread_synchronized_method;

/**
   定义账户类
 */
public class Account {
    private double money; // 账户的余额

    public Account(double money) {
        this.money = money;
    }

    public Account() {
    }

    public double getMoney() {
        return money;
    }

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

    /**
     * 取钱功能
     * @param money:取钱的金额
     */
    public synchronized void drawMoney(double money){
        // 1.先获取是谁来取钱,线程的名字设置的是人名
        String name = Thread.currentThread().getName();
            // 2.判单账户的余额 >= 取钱的金额
            if(this.money >= money){
                // 可以取钱了
                System.out.println(name + "取钱成功,取出" + money + "元!");
    //            setMoney(getMoney() - money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后共享账户剩余:" + this.money);
            }else{
                System.out.println(name + "来取钱,账户余额不足");
            }
        }
    }

package com.gch.d6_thread_synchronized_method;

/**
   取钱的线程类
 */
public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;

    /**
     * 有参构造器
     * @param acc:接共享的账户对象
     * @param name:线程名
     */
    public DrawThread(Account acc, String name) {
        super(name);
        this.acc = acc;
    }

    public DrawThread() {
    }

    public Account getAcc() {
        return acc;
    }

    public void setAcc(Account acc) {
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明、小红:取钱
        acc.drawMoney(100000);
    }
}
package com.gch.d6_thread_synchronized_method;


/**
   需求:模拟取钱案例
 */
public class ThreadSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 测试线程安全问题
        // 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
      Account acc = new Account(100000);

        // 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
        // 直接new对象这叫匿名对象
        new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);
        new DrawThread(acc,"小红").start();
    }
}

线程安全、线程同步(同步代码块、同步方法、同步锁)_第13张图片

  1.  同步方法是如何保证线程安全的?
  • 对出现问题的核心方法使用synchronized修饰
  • 每次只能一个线程占锁进入访问
  1. 同步方法的同步锁对象的原理?
  • 同步方法的底层是有隐式锁对象的,只是锁的范围是整个方法代码块!
  • 对于实例方法默认使用this作为锁对象。
  • 对于静态方法默认使用当前类的字节码文件,类名.class对象作为锁对象​​​

 同步方法案例二:卖票

  • 不要去写同步方法,先写同步代码块,然后再把同步代码块里面的代码,去抽取成方法,这就OK了!
package com.gch.d3_thread_safe_2;
/**
   线程任务类
 */
public class MyRunnable implements Runnable {
    // MyRunnable对象只创建一次,因此变量票数面前无需加static
    int ticket = 0; // 0 ~ 99

    @Override
    public void run() {
        // 1.循环
        while(true){
            // 2.同步方法
                if (method()) break;
        }
    }

    // 锁对象:this
    private synchronized boolean method() {
        // 3.判断共享数据是否到了末尾,如果到了末尾
        if(ticket == 100){
            return true;
        }else{
            // 4.判断共享数据是否到了末尾,如果没有到末尾
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!" );
        }
        return false;
    }
}
package com.gch.d3_thread_safe_2;

public class ThreadDemo {
    public static void main(String[] args) {
       /*
        案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
                请设计一个程序模拟该电影院卖票
       */

        // 1.创建线程任务对象
        Runnable target = new MyRunnable();

        // 2.创建线程对象
        Thread t1 = new Thread(target,"窗口1");
        Thread t2 = new Thread(target,"窗口2");
        Thread t3 = new Thread(target,"窗口3");

        // 3.启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

 补充知识:StringBuilder和StringBuffer的区别

  1. StringBuilder和StringBuffer的API一模一样。
  2. StringBuilder是线程不安全的,StringBuffer是线程安全的。
  3. StringBuffer的源码里面方法都是同步方法,加了synchronized修饰。
  4. 使用场景区分:
  • 如果你的代码是单线程的,不需要考虑多线程当中数据安全的抢矿,你就用StringBuilder就可以了。
  • 如果说你是多线程环境下需要考虑数据安全,那么就可以选择StringBuffer。

 2.4 方式三:Lock锁

  • 有了Lock锁我们就可以手动的上锁,还有手动的释放锁了!

线程安全、线程同步(同步代码块、同步方法、同步锁)_第14张图片

package com.gch.d7_thread_synchronized_lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
   定义账户类
 */
 public class Account {
    private double money; // 账户的余额
    // 加了final的变量只能被初始化一次!
    // final修饰后:锁对象是唯一和不可替换的,非常专业
    private final Lock lock = new ReentrantLock(); // 实例成员变量,每创建一个账户对象就创建一个锁对象

    public Account(double money) {
        this.money = money;
    }

    public Account() {
    }

    public double getMoney() {
        return money;
    }

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

    /**
     * 取钱功能
     * @param money:取钱的金额
     */
    public void drawMoney(double money){
//        lock = null; 直接报错,因为锁对象被final修饰,是唯一的不可替换的
        // 1.先获取是谁来取钱,线程的名字设置的是人名
        String name = Thread.currentThread().getName();
            // 2.判单账户的余额 >= 取钱的金额
        lock.lock(); // 上锁
        try {
            if(this.money >= money){
                // 可以取钱了
                System.out.println(name + "取钱成功,取出" + money + "元!");
    //            setMoney(getMoney() - money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后共享账户剩余:" + this.money);
            }else{
                System.out.println(name + "来取钱,账户余额不足");
            }
        } finally { // 加了finally,解锁更加安全,即使出了bug也会解锁
            lock.unlock(); // 解锁
        }
    }
}

package com.gch.d7_thread_synchronized_lock;

/**
   取钱的线程类
 */
public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;

    /**
     * 有参构造器
     * @param acc:接共享的账户对象
     * @param name:线程名
     */
    public DrawThread(Account acc, String name) {
        super(name);
        this.acc = acc;
    }

    public DrawThread() {
    }

    public Account getAcc() {
        return acc;
    }

    public void setAcc(Account acc) {
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明、小红:取钱
        acc.drawMoney(100000);
    }
}
package com.gch.d7_thread_synchronized_lock;


/**
   需求:模拟取钱案例
 */
public class ThreadSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 测试线程安全问题
        // 1.定义账户类,创建一个账户对象代表2个人共享的账户对象
      Account acc = new Account(100000);

        // 2.定义线程类,创建两个线程对象,代表小明和小红同时进来了
        // 直接new对象这叫匿名对象
        new DrawThread(acc,"小明").start();
//        DrawThread.sleep(30);
        new DrawThread(acc,"小红").start();
    }
}

线程安全、线程同步(同步代码块、同步方法、同步锁)_第15张图片

Lock锁案例二:卖票

package com.gch.d3_thread_safe_3;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread {
    /**
     * 调用父类Thread的有参构造器
     * @param name:线程名
     */
    public MyThread(String name){
        super(name);
    }
    // 表示本类 / 当前类的所有对象,都共享ticket数据
    public static int ticket = 0; // 0 ~ 99
    // 定义Lock锁,锁对象必须是唯一和不可替换的
    // 静态成员变量,本类 / 当前类的所有对象共享一把锁
    // 如果定义成实例成员变量,那么每创建一个线程对象就创建一把锁,窗口1,窗口2,窗口3各自都有各自的锁,各自卖各自的
    private static final Lock lock = new ReentrantLock();
    @Override
    public void run() {
        // 1.循环
        while(true){
            // 2.上锁
            lock.lock();
            try {
                // 3.判断
                if(ticket == 100){
                    break; // 如果ticket == 100,将会直接跳出循环,这导致的结果就是没有释放锁!!!程序运行就会出现bug
                }else{
                    // 4.判断
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                }
            } finally {
                // 4.解锁
                lock.unlock();
            }
        }
    }
}
package com.gch.d3_thread_safe_3;


public class ThreadDemo {
    public static void main(String[] args) {
       /*
        案例需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,
                请设计一个程序模拟该电影院卖票
         用JDK5的Lock实现
       */

        // 1.创建线程对象
        Thread t1 = new MyThread("窗口1");
        Thread t2 = new MyThread("窗口2");
        Thread t3 = new MyThread("窗口3");

        // 2.启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

线程安全、线程同步(同步代码块、同步方法、同步锁)_第16张图片

 

你可能感兴趣的:(Java,jvm,java,分布式)