JavaSE学习笔记25:多线程(三)

多线程(三)

线程安全

什么时候数据在多线程并发的环境下会存在安全问题

  1. 多线程并发
  2. 有共享数据
  3. 共享数据有修改行为

满足以上3个条件之后,就会存在线程安全问题

同步机制

如何解决?线程排队执行(不能并发),专业术语叫做“线程同步机制”

​ 用排队执行解决线程安全问题,这种机制被称为:线程同步机制。

注意:线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位。只有数据安全了,我们才可以谈效率,数据不安全,没有效率的事。

说到线程同步这块,涉及到两个专业术语:

  1. 异步编程模型:

    线程T1和线程T2,各自执行各自的,T1不管T2,T2不管T1,谁也不需要等谁,其实就是多线程并发(效率较高)

  2. 同步编程模型:

    线程T1和T2,在线程T1执行的时候,必须等待T2线程执行结束,或者说在T2线程执行的时候,必须等待T1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。(效率较低,线程排队执行)

异步就是并发,同步就是排队!!!

synchronized的写法:

  1. 同步代码块
synchronized(线程共享对象){
    同步代码块;
}
  1. 在实例方法上使用synchronized。表示共享对象一定是this,并且同步代码块是整个方法体。

  2. 在静态方法上使用synchronized。表示找类锁,类锁永远只有1把。

    对象锁:1个对象1把锁,100个对象100把锁。

    类锁:100个对象,也可能只是1把类锁

例子1
package se5.threadsafe;

public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建一个)
        Account act = new Account("act-001",10000);
        //创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();
        /**
         * 结果:
         * t1对act-001取款5000.0成功,余额5000.0
         * t2对act-001取款5000.0成功,余额5000.0
         */
    }
}
package se5.threadsafe;
/*
银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
 */
public class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account() {}
    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){
        //t1和t2并发这个方法。。。(t1和t2是两个栈,两个栈操作堆中同一个对象)
        //取款之前的余额
        double before = this.getBalance();
        //取款之后的余额
        double after = before - money;

        //在这里模拟一下网络延迟,100%出问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        //思考:t1执行到这里了,但还没有执行这行代码,t2线程进来withdraw方法了,此时一定出问题。
        this.setBalance(after);
    }
}
package se5.threadsafe;

public class AccountThread extends Thread {
    //两个线程必须共享同一个账户对象
    private Account act;
    //通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }
    public void run(){
        //run方法的执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        //多线程并发执行这个方法
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功,余额" + act.getBalance());
    }
}

修改后

package se5.threadsafe2;

public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建一个)
        Account act = new Account("act-001",10000);
        //创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();
        /**
         * 结果:
         * t1对act-001取款5000.0成功,余额5000.0
         * t2对act-001取款5000.0成功,余额0.0
         */
    }
}
package se5.threadsafe2;
/*
银行账户
    使用线程同步机制,解决线程安全问题
 */
public class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account() {}
    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){
        //以下这几行代码必须是线程排队的,不能并发
        //一个线程把这里的代码全部执行结束之后,另一个线程才能进来
        /**
         * 线程同步机制的语法是:
         *  synchronized(){
         *      //线程同步代码块
         *  }
         *  synchronized后面小括号中传的数据是相当关键的,这个数据必须是多线程共享的数据,才能达到多线程排队。
         *  ()写什么?
         *      那要看你想让哪些线程同步。
         *      假设t1、t2、t3、t4、t5有5线程
         *      只希望t1、t2、t3排队,t4、t5不需要排队
         *      一定要在小括号中写一个t1、t2、t3共享的对象,而这个对象对于t4、t5来说不是共享的。
         *
         *  这里的共享对象是:账户对象
         *  账户对象是共享的,那么this就是账户对象!!!
         *  不一定是this,这里只要是多线程共享的对象就行。
         *
         *  以下代码执行原理:
         *      1.假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
         *      2.假设t1先执行了,遇到了synchronized,这个时候自动找后面共享对象的对象锁,找到之后,
         *并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代
         *码块代码结束,这把锁才会释放。
         *      3.假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的
         *这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,
         *t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
         *
         *     这样就达到了线程排队执行。
         *     这里需要注意:共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象锁共享的。
         */
        synchronized (this){
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }
}
package se5.threadsafe2;

public class AccountThread extends Thread {
    //两个线程必须共享同一个账户对象
    private Account act;
    //通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }
    public void run(){
        //run方法的执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        //多线程并发执行这个方法
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功,余额" + act.getBalance());
    }
}
例子2
package com.thread2;

//不安全的买票
//线程不安全,有负数
public class UnsafeBuyTicket {
    public static void main(String[] args) {

        BuyTicket station = new BuyTicket();

        new Thread(station,"黄牛张三").start();
        new Thread(station,"黄牛李四").start();
        new Thread(station,"黄牛王五").start();
        /**
         * ...
         * 黄牛王五拿到3
         * 黄牛李四拿到2
         * 黄牛张三拿到1
         * 黄牛王五拿到-1
         * 黄牛李四拿到0
         */
    }
}
class BuyTicket implements Runnable{
    //票
    private int ticketNums = 10;
    boolean flag = true;//外部停止方式

    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }
    private void buy(){
        //判断是否有票
        if (ticketNums <= 0){
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //买票
        System.out.println(Thread.currentThread().getName()+ "拿到" + ticketNums--);
    }
}

使用synchronized修改后

package com.thread2;

public class UnsafeBuyTicket {
    public static void main(String[] args) {

        BuyTicket station = new BuyTicket();

        new Thread(station,"黄牛张三").start();
        new Thread(station,"黄牛李四").start();
        new Thread(station,"黄牛王五").start();
        /**
         * 黄牛张三拿到10
         * 黄牛张三拿到9
         * 黄牛张三拿到8
         * 黄牛李四拿到7
         * 黄牛李四拿到6
         * 黄牛李四拿到5
         * 黄牛王五拿到4
         * 黄牛李四拿到3
         * 黄牛李四拿到2
         * 黄牛李四拿到1
         */
    }
}
class BuyTicket implements Runnable{
    //票
    private int ticketNums = 10;
    boolean flag = true;//外部停止方式

    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }
    //synchronized 同步方法,锁的是this
    private synchronized void buy(){
        //判断是否有票
        if (ticketNums <= 0){
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //买票
        System.out.println(Thread.currentThread().getName()+ "拿到" + ticketNums--);
    }
}

死锁

死锁不会出现异常,也不会出现错误,程序一直僵持在那里。这种错误最难调试。

package com.thread2;

//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
    public static void main(String[] args) {

        Makeup g1 = new Makeup(0,"女孩1");
        Makeup g2 = new Makeup(1,"女孩2");

        g1.start();
        g2.start();
    }
}
//口红
class Lipstick{}
//镜子
class Mirror{}

class Makeup extends Thread {
    //需要的资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//选择
    String girlName;//使用化妆品的人

    Makeup(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }
    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {//获得口红的锁
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(1000);

                synchronized (mirror) {//一秒后想获得镜子
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) {//获得镜子的锁
                System.out.println(this.girlName + "获得镜子的锁");
                Thread.sleep(2000);

                synchronized (lipstick) {//两秒后想获得口红
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }
    }
}

上面这个程序执行结果会一直僵持,停不下来。

Lock锁

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。

class A{
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码
        }
    finally{
        	lock.unlock();
        	//如果同步代码有异常,要将unlock()写入finally语句块
    	   }
    }
}
package com.thread2;

import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock {
    public static void main(String[] args) {

        TestLock2 testLock2 = new TestLock2();

        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}
class TestLock2 implements Runnable {

    int ticketNums = 10;

    //定义Lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        while (true){
            try {
                lock.lock();//加锁
                if (ticketNums > 0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                }else {
                    break;
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

synchronized与Lock的对比

  1. Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,处理作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

关于wait和notify方法

  1. wait和notify方法不是线程对象的方法,是java任何一个java对象都有的方法,因为这两个方法是Object类中自带的。

    wait方法和notify方法不是通过线程对象调用的

  2. wait()方法的作用

    Object o = new Object();
    o.wait();
    /*
    表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
    o.wait();方法的调用,会让当前线程(正在o对象上活动的线程)进入等待状态,
    wait(long timeout):指定等待的毫秒数
    */
    
  3. notify()方法的作用

    Object o = new Object();
    o.notify();
    /*
    表示:唤醒正在o对象上等待的线程
    还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程
    */
    

生产者和消费者模式

  1. 使用wait方法和notify方法实现生产者和消费者模式

  2. 什么是生产者和消费者模式?

    生产线程负责生产,消费线程负责消费
    生产线程和消费线程要达到均衡
    这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法

  3. wait和notify方法不是线程对象的方法,是普通java对象都有的方法

  4. wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题

  5. wait方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且在释放掉t线程之前占有o对象的锁

  6. notify方法的作用:o.notify()让正在o对象上等待的线程被唤醒,只是通知,不会释放o对象上之前占有的锁

例子:

模拟:仓库采用list集合,list集合中假设只能存储1个元素,1个元素就表示仓库满了,如果list集合中元素个数是0,就表示仓库空了。保证list集合中永远都是最多存储1个元素,必须做到这种效果:生产1个消费1个。

package se5.thread;

import java.util.ArrayList;
import java.util.List;

public class ThreadTest16 {
    public static void main(String[] args) {

        //创建一个仓库对象,共享的
        List list = new ArrayList();
        //创建两个线程对象
        //生产者线程
        Thread t1 = new Thread(new Producer(list));
        //消费者线程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生产者线程");
        t2.setName("消费者线程");

        t1.start();
        t2.start();
    }
}
//生产线程
class Producer implements Runnable{

    //仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        //一直生产(使用死循环模拟)
        while (true){
            //给仓库对象list加锁
            synchronized (list){
                if (list.size() > 0){//大于0,说明仓库中已经有1个元素了
                    //当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                //唤醒消费者进行消费
                //list.notify();
                list.notifyAll();
            }
        }
    }
}
//消费线程
class Consumer implements Runnable{

    //仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        //一直消费
        while (true){
            synchronized (list){
                if (list.size() == 0){
                    try {
                        //仓库空了
                        //消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能执行到此处说明仓库中有数据,进行消费
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                //唤醒生产者进行生产
                //list.notify();
                list.notifyAll();
            }
        }
    }
}
posted @ 2020-11-25 16:25  最爱琴女E  阅读(120)  评论(0)    收藏  举报