多线程(下)

136 阅读5分钟

前面提到过的单例模式中的懒汉式是线程不安全的,那么我们用刚学的知识来使懒汉式变为线程安全的。

//这即为懒汉式
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        if(instance==null){
            instance = new Bank();

        }
        return instance;
    }
}

同步方法:

在Bank getInstance方法前加上synchronized即可使线程安全

此时的同步监视器为Bank.class

同步代码块

//这即为懒汉式
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        synchronized (Bank.class) {
            if (instance == null) {
                instance = new Bank();

            }
            return instance;
        }
    }
}

此时的同步监视器为Bank.class

上面两种方式效率稍差

可以这样想:将null想成一件商品,有许多人想买这件商品,因此就得抢啊,先抢到的人先进去并且锁上了门。等到他买完了并且把锁打开但从后门走掉了,因此后面的人都只能一个个进来并且由于商品已售出只能从后门走掉,那么就会浪费很多时间

因此,要更改上述效率变高,得做出以下更改

class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
    if (instance == null) {
        synchronized (Bank.class) {
            if (instance == null) {
                instance = new Bank();
            }
            }
            return instance;
        
        }
    }
}

效率稍高

线程的死锁问题

死锁:

1.不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

2.出现死锁后,不会出现异常,提示,只是所有的线程都处于阻塞状态,无法继续

解决方法:

1.专门的算法,原则

2.尽量减少同步资源的定义

3.尽量避免嵌套同步

转化为现实问题就是:

两个人吃饭,一个人吃完把筷子给另一个人,但两个人各拿了一根筷子,且都在等待对方把另一根筷子给自己,这就造成了线程的死锁。

(好像不是很恰当,凑合着看)

//演示线程的死锁问题
public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

上图中可能会出现线程的死锁问题,但是几率很小。

因此用sleep来提高几率。

那么,在原来代码的基础上,分别在两种方式的第二个锁之前加入Thread.sleep(100);

分析:当方式1进去时,拿着s1锁并且进入睡眠。此时方式2也会进去,那么就会拿着s2锁进入睡眠。两个几乎是同时醒来的,那么就会导致互相握着锁且在另一个锁。那么就会出现死锁问题。

解决线程安全问题的方式3

Lock(锁)-->JDK5.0新增

image.png

1.实例化ReentrantLock

通过private ReentrantLock lock = new ReentrantLock();来创建lock也就是锁。()无参的话默认false,若填入true说明此锁很公平,各个线程握到的锁不仅机会均等且不会重复。

2.创建一个try finally,将需要单线程的代码包在try中,并在try下的开头处通过lock.lock();来调用锁定方法lock()。

在finally中通过lock.unlock();调用解锁方法unlock()

//解决线程安全问题的方式3
class Window5 implements Runnable{
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":售票:票号为" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window5 w = new Window5();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

面试题:

synchronized与Lock的异同?

同:二者都可以解决线程安全问题

不同:synchronized机制在执行完相应的同步代码后,自动的释放同步监视器

Lock需要手动的启动同步(lock()),同时结束同步也需要手动实现(unlock)

image.png

练习1:

image.png

1.是否是多线程问题?

是,2个储户线程

2.是否有共享数据?

有,账户余额

3.是否有线程安全问题?

4.需要考虑如何解决线程安全问题?

同步机制:3种方式

1.同步代码块

2.同步方法

3.Lock

好的,分析完了,来做题

class Account{
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }
    //创建一个存钱的方法
    public void deposit(double amt){
        if(amt>0){
            balance+=amt;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存钱成功,当前余额为:"+ balance);
        }

    }
}
class Customer extends Thread{
    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for(int i = 0;i < 3;i++){
            acct.deposit(1000);
        }
    }
}



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


        Account acct = new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}

上图为线程不安全的做法

此时输出

甲存钱成功,当前余额为:2000.0 乙存钱成功,当前余额为:2000.0 乙存钱成功,当前余额为:4000.0 甲存钱成功,当前余额为:4000.0 乙存钱成功,当前余额为:6000.0 甲存钱成功,当前余额为:6000.0

顺序不一定是这样

解决方法:

1.同步方法:直接在deposit()方法前加上synchronized

其他两种就不一一列举了

线程的通信:

涉及到3个方法:

wait方法:一旦执行wait了,当前线程进入阻塞状态,需要另一个线程调用notify方法才能重新换发活力,会释放锁

notify方法:一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个

notifyAll方法:一旦执行此方法,就会唤醒所有被wait的线程

说明:

1.上述3个方法必须使用在同步代码块或同步方法中

2.3个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现异常

3.上述3个方法是定义在Object类中的

通过例题说明:

/线程通信的例子使用2个线程打印1-100,线程1,线程2交替打印
class Number implements Runnable{
    private int number = 1;

    @Override
    public void run() {
        while(true) {
            synchronized (this) {
                notify();
                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下方法的线程进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number n = new Number();
        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

也可以使用lock的方法且设置

private ReentrantLock lock = new ReentrantLock (true) ;

import java.util.concurrent.locks.ReentrantLock;

class Number1 implements Runnable{
    private int number = 1;
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while(true) {

                try {
                    lock.lock();
                    if (number <= 100) {
                        System.out.println(Thread.currentThread().getName() + ":" + number);
                        number++;
                    } else {
                        break;

                    }
                }finally {
                    lock.unlock();
                }
        }
    }
}
public class CommunicationTest1 {
    public static void main(String[] args) {
        Number1 n = new Number1();
        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

面试题:

sleep()和wait()方法的异同:

1.相同点:一旦执行方法,都可以使得当前线程进入阻塞状态

2.不同点:

1)两个方法声明的位置不一样:Thread类中声明sleep(),Object类中声明wait()

2)调用的要求不同:sleep可以在任意场景下调用,wait必须使用在同步代码块或同步方法中

3)关于是否释放同步监视器:如果2个方法都使用在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁

那么,上题!

image.png

先分析一把:

1.是否是多线程问题?是,生产者线程,消费者线程

2.是否有共享数据?是,产品(或店员)

3.如何解决线程安全问题?同步机制,有3种方法

4.是否涉及到线程通信?是(得使用wait,notifty)

class Clerk{
    private int productCount = 0;
    public synchronized void produceProduct() {//生产产品
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount+"个产品");
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void consumeProduct() {//消费产品
        if(productCount>0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount+"个产品");
            productCount--;
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread{

    private Clerk clerk;
    public Producer(Clerk clerk){//生产者
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品......");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}
class Consumer extends Thread{//消费者
    private Clerk clerk;
    public Consumer(Clerk clerk){//生产者
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品......");
        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}




public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1= new Producer(clerk);
        p1.setName("生产者1");
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        p1.start();
        c1.start();
    }
}

可设置sleep睡眠的时间来加强呈现性

JDK5.0新增线程创建方式

新增方式1:实现Callable的接口

与使用Runnable接口相比,Callable接口功能更加强大

1.相比run方法,可以有返回值

2.方法可以抛出异常

3.支持泛型的返回值

4.需要借助FutureTask类,比如获取返回结果

image.png

如何理解Callable比Runnable强大:

1.call方法有返回值

2.call方法可以抛出异常,被外面的操作捕获,获取异常的信息

3.Callable是支持泛型的

新增方式2:使用线程池

image.png

去外面玩,其他手动方式创建线程相当于自己造一辆车子并且在到达后将车子进行销毁。而使用线程池则是搭公共汽车去到那且不用销毁

image.png