线程学习笔记(二)

线程学习笔记(一)

目录

死锁

锁:可重入的互斥锁 Lock

多线程练习题

线程通信:wait(),notifyAll()的使用

sleep与wait区别

生产者/消费者问题

创建线程的第三种方式:实现Callable接口

Callable/Future原理理解


死锁

可以看这篇:https://blog.csdn.net/hd12370/article/details/82814348

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

public class deadlock {
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        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() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("b");
                    s2.append("3");
                    synchronized (s1) {
                        s1.append("c");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

结果,两种可能 

ab
12
abbc
1234
bc
34
bcab
3412

如果都加上sleep(100) ,就会出现死锁

public class deadlock {
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        new Thread() {
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //需要得到s2这把锁,下面的线程却在使用,等待
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("b");
                    s2.append("3");
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //需要得到s1这把锁,上面的线程却在使用,等待
                    synchronized (s1) {
                        s1.append("c");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

结果:因为互相等待,所以导致死锁,不打印。

锁:可重入的互斥锁 Lock

这篇文章写得不错看这篇:https://www.cnblogs.com/takumicx/p/9338983.html

可重入:https://www.cnblogs.com/gxyandwmm/p/9387833.html

可重入就是,就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值(state)进行累加(+1),当前线程的state为0了,其他线程(如果是非公平锁策略,包括自己)才可以来竞争这把锁。

还是那个窗口售票的例子,只要两步就可以实现多线程问题

public class LockTest {
    public static void main(String[] args) {
        window window = new window();
        Thread thread1 = new Thread(window);
        Thread thread2 = new Thread(window);
        Thread thread3 = new Thread(window);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class window implements Runnable {
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (ticket > 0) {
            buy();
        }
    }

    public void buy() {
        try {
            //获取锁
            lock.lock();
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "=" + ticket--);
            }
        } finally {
            //释放锁
            lock.unlock();
        }

    }
}

多线程练习题

银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000, 存3次。每次存完打印账户余额。

class Account {
    //默认原始账户余额0元
    private int balance = 0;

    public void deposit(int money) {
        if (money > 0) {
            balance += money;
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存款了"+money+"元,当前账户余额:" + balance);
        }
    }
}


class Customer extends Thread {
    //使用继承Thread ,账户应共享一个,所以公用Account 对象
    private Account account;

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

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


public class Bank {
    public static void main(String[] args) {
        Account account = new Account();
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}

结果:明显不正确,第一次打印应该是余额1000元 等等,因为在睡眠1秒的过程中当前线程进入阻塞态,其他线程进来了,变为运行态,这样,输出语句没来得及打印,账户就被加了2次1000。

乙存款了1000元,当前账户余额:2000
甲存款了1000元,当前账户余额:2000
甲存款了1000元,当前账户余额:4000
乙存款了1000元,当前账户余额:5000
乙存款了1000元,当前账户余额:6000
甲存款了1000元,当前账户余额:6000

解决办法,加锁,为啥ReentrantLock 不用加static呢,因为两个线程同步监视器都是accout对象。

class Account {
    //默认原始账户余额0元
    private int balance = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void deposit(int money) {
        //加锁
        lock.lock();
        if (money > 0) {
            balance += money;
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存款了" + money + "元,当前账户余额:" + balance);
        }
        //解锁
        lock.unlock();
    }
}

线程通信:wait(),notifyAll()的使用

来看个案例:使用两个线程打印 1-100。线程1, 线程2 交替打印

public class PrintAlternately {
    public static void main(String[] args) {
        print print = new print();
        Thread t1 = new Thread(print);
        Thread t2 = new Thread(print);
        t1.start();
        t2.start();
    }
}

class print implements Runnable {
    private int num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                //每次进来都唤醒所有线程,不然就死锁了
                notify();
                if (num <= 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "=" + num);
                    num++;
                    try {
                        //让当前线程等待进入阻塞状态,并释放锁
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else break;
            }
        }
    }
}
Thread-0=1
Thread-1=2
Thread-0=3
Thread-1=4
......
Thread-0=97
Thread-1=98
Thread-0=99
Thread-1=100

这个我之前有写错,逻辑没搞清楚,分享下2个错误写法,为什么错误。

public void run() {
        while (num <= 2) {
            synchronized (this) {
                notify();
                if (num <= 2) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "=" + num);
                    num++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else break;
            }
            System.out.println(Thread.currentThread().getName() + "while里=" + num);
        }
        System.out.println(Thread.currentThread().getName() + "while外=" + num);
    }

为了方便查看,把打印到100全都调到2

第一种错误写法,把 while (true)改成while (num <= 2)

  1. 会出现以下结果,发现程序还没停止,因为改成这样
  2. 打印完Thread-1=2,线程Thread-1被阻塞了
  3. 这时切换到Thread-0,走出if,打印while里,然后回到while循环发现num=3,不满足循环,跳出while,打印while外,Thread-0生命结束
  4. 会发现Thread-1还被阻塞,竟然没有被唤起,因为进不到synchronized代码块执行唤醒notify(),所以没打印了

所以,while (true)就是为了,下一个线程再进来,把所有线程唤醒,这只有俩线程所以用notify(),如果多个要改成notifyAll()。

第二种错误写法,是改成ture了,但是没有break语句

 public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (num <= 2) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "=" + num);
                    num++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

发现程序没退出,为啥呢

  • 因为陷入死循环,没有跳出while循环语句,所以是该把打印的打印完了,但是却还在while里面一直循环,如果在if语句外面写个输出语句会发现会一直输出。 

所以,我发现多线程代码,真的基础逻辑要好,不然真的死定了,要是在真实生产环境,陷入死循环,估计内存爆满,直接炸。

sleep与wait区别

相同点

  • 一 旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点

  • 两个方法声明的位置不同: Thread类中声明sLeep(),object类 中声明wait()
  • 调用的要求不同: sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
  • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁
     

生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。


这里可能出现两个问题: 

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  2. 消费者比生产者快时,消费者会取相同的数据。

分析:

  1. 无识别结果程问题?是,生产者线程,消费者线程
  2. 是否有共享数据?是,店员(或产品)
  3. 如何解决线程的安全问题?同步机制,有三种方法
  4. 是否涉及线程的通信?是
package producer_consumer_question;

//店员
class Clerk {
    //商品
    private int goods = 0;

    public synchronized void productionGoods() {
        if (goods < 20) {
            goods++;
            System.out.println(Thread.currentThread().getName() + "号生产者开始生产第" + goods + "件商品");
            notifyAll();
        }else {
            try {
                //大于等于停止生产。线程停止
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consumerGoods() {
        if (goods > 0) {
            System.out.println(Thread.currentThread().getName() + "号消费者开始消费第" + goods + "件商品");
            goods--;
            //唤醒生产者或消费者线程
            notifyAll();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer implements Runnable {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.productionGoods();
        }
    }
}

class Consumer implements Runnable {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumerGoods();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        Thread p1 = new Thread(producer);
        Thread p2 = new Thread(producer);
        Thread c1 = new Thread(consumer);
        Thread c2 = new Thread(consumer);
        p1.setName("生产者1");
        p2.setName("生产者2");
        c1.setName("消费者1");
        c2.setName("消费者2");
        p1.start();
        p2.start();
        c1.start();
        c2.start();
    }
}
生产者2号生产者开始生产第1件商品
消费者1号消费者开始消费第1件商品
生产者1号生产者开始生产第1件商品
消费者2号消费者开始消费第1件商品
生产者2号生产者开始生产第1件商品
生产者1号生产者开始生产第2件商品
消费者2号消费者开始消费第2件商品
消费者1号消费者开始消费第1件商品
......

创建线程的第三种方式:实现Callable接口

通过实现Callable接口的call方法,可以返回异步计算的结果,结果由FutureTask来接受

以下是,通过两个线程来计算1到100的值,我很奇怪为什么两个线程的值会相同,不是有一个线程会提前结束吗,感觉不好理解。

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //3.创建Callable接口实现类的对象
        Cal cal = new Cal();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask 的对象
        FutureTask<Integer> f1 = new FutureTask<Integer>(cal);
        FutureTask<Integer> f2 = new FutureTask<Integer>(cal);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象, 并调用start()
        new Thread(f1).start();
        new Thread(f2).start();
        a.setName("A线程");
        b.setName("B线程");
        a.start();
        b.start();
        Integer sum1 = f1.get();
        Integer sum2 = f2.get();
        System.out.println("sum1:"+sum1);
        System.out.println("sum2:"+sum2);
    }
}
//1.创建一个实现Callable的实现类
class Cal implements Callable {
    private int num = 1;
    private int sum = 0;

    
    //2.实现call方法,将此线程需要执行的操作声明在call中
    @Override
    public Object call() throws Exception {
        for (; ; ) {
            synchronized (this) {
                if (num <= 100) {
                    System.out.println(Thread.currentThread().getName() + "=" + num);
                    sum += num++;
                } else break;
            }
        }
        return sum;
    }
}
......
B线程=95
A线程=96
A线程=97
B线程=98
A线程=99
B线程=100
sum1:5050
sum2:5050

Callable/Future原理理解

一定先看这个https://www.jianshu.com/p/4974e445c6d6,在看我的讲解,就会很清楚,这篇文章讲解Callable/Future源码解析基本很清晰了,再加上自己动手看源码,基本就懂了。

里面涉及到Unsafe,看这俩篇https://www.jianshu.com/p/2e5b92d0962ehttps://www.jianshu.com/p/db8dce09232d或者再看看别的

自己的理解:

我们需要知道的各类之间关系,UML图如下

整个大概执行过程 :

  1. 我们自定义的cal类通过实现Callable的call方法,写出自己的逻辑,和要返回的值
  2. 然后在使用的时候通过FutureTask类的带参---cal,构造初始化一个FutureTask
  3. 通过Thread带参--futureTask,实例化一个Thread启动线程
  4. 然后上面执行完,通过futureTask.get获取异步计算的结果

结论:实现Callable最后也要通过Thread类来启动线程,那FutureTask怎么获取计算结果的呢?

大致过程

  1. FutureTask是继承Runable,主要还是通过执行run()方法,所以start()方法一执行,就会先执行run()
  2. 在run()方法里调用Callable的call()方法,得到结果
  3. 然后把结果给FutureTask的自带属性outcome
  4. 在通过get()方法获取到这个outcome。

注意:get 方法是阻塞获取线程执行结果,当在main中调用f1.get(),这时其实是在等待另一个线程----FutureTask线程计算好结果。

向线程池中提交任务的submit方法不是阻塞方法,而Future.get方法是一个阻塞方法,当submit提交多个任务时,只有所有任务都完成后,才能使用get按照任务的提交顺序得到返回结果

我们来看下FutureTask主要的几个关键属性方法,怎么获取结果的过程,在代码里标好顺序了。

public class FutureTask<V> implements RunnableFuture<V> {
    //返回结果或抛出异常的结果,就是call要返回的值
    private Object outcome;

    //1.start执行,run就会执行
    public void run() {
        if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                        null, Thread.currentThread()))
            return;
        try {
            //2.构造函数new FutureTask<Integer>(cal)里面的cal会赋值给c
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //3.执行自定义的call(),并获取返回结果
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    //4.如果异常就把异常设置给结果
                    setException(ex);
                }
                if (ran)
                    //4.如果正常就把result设置给outcome
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
    
    //设置outcome的方法
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            //5.把v就是call的结果赋值给outcome
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
    
    //get方法,返回计算后的结果
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        //6.程序调用get获取结果,就调用report()获取outcome
        return report(s);
    }

    //获取计算好的结果
    private V report(int s) throws ExecutionException {
        //7.结果已经有值了,赋给x
        Object x = outcome;
        if (s == NORMAL)
            //8.最后把结果x返回调用者get
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值