Java基础之多线程

一、线程对象的生命周期

1. 概述

在这里插入图片描述

private String content;
@Test
public void fun01(){
    Thread t = new Thread(() ->{
        content = "Hello World!";
    });
    t.start();
    System.out.println(content.toUpperCase());
}

上面是一个简单的案例,我们可以通过这个案例来理解线程对象的生命周期。

  1. 使用new关键字来创建一个线程对象
  2. t.start()表示线程对象进入就绪状态,就绪并不代表运行,只是说可以参与CPU使用权的竞争。
  3. running才是运行状态,表示通过竞争当前线程得到了CPU的使用权,并且正在运行中

上述代码运行后会报一个空指针异常:

在这里插入图片描述

这是因为我们的代码在运行到输出语句的时候线程对象还没有处于运行状态。

private String content;
@Test
public void fun01() throws Exception{
    Thread t = new Thread(() ->{
        content = "Hello World!";
    });
    t.start();
    Thread.sleep(1000);
    System.out.println(content.toUpperCase());
}

为了让我们创建的线程对象在主线程之前运行,我们可以使用Thread.sleep()方法让主线程休眠,这时主线程处于阻塞状态,但是并不是一直处于阻塞状态,因为sleep方法是有参数的,我们可以传入参数来控制线程休眠的时间。

通过上述操作我们可以实现我们的需求,但是效果不理想,因为我们虽然让主线程休眠了1000毫秒但是我们不知道我们创建的线程需要多久才能执行完。

private String content;
@Test
public void fun01() throws Exception{
    Thread t = new Thread(() ->{
        content = "Hello World!";
    });
    t.start();
    while (content == null);
    System.out.println(content.toUpperCase());
}

我们可以对代码进行进一步的改进,在主线程内写一个while循环,然后判断是否符合条件,只有符合条件才往下运行,否则一直循环(在循环过程中另一个线程仍然和它处于竞争状态,当另一个线程获得CPU使用权时就会执行另一个线程的代码)。

private String content;
@Test
public void fun01() throws Exception{
    Thread t = new Thread(() ->{
        content = "Hello World!";
    });
    t.start();
    while (content == null){
        Thread.yield();
    };
    System.out.println(content.toUpperCase());
}

为了提高效率,我们还可以在循环体中添加Thread.yield()方法,该方法可以将当前的运行线程修改为就绪状态,从而让其它线程有运行的机会。

Thread.yield()方法作用:

​ 暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:

​ yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状 态转到可运行状态,但有可能没有效果。

2. 线程阻塞状态

线程的阻塞状态有很多种,我们在前面的案例中也提到了一些:

  • sleep():计时等待状态,在设置的时间内,该线程将会处于阻塞状态
  • wait():无限等待状态,如果不被其它线程唤醒,将一直处于阻塞状态
  • join():该方法可以让某一线程先运行,等这个线程运行结束其它线程再运行

3. 线程优先级

Thread t = new Thread();
//设置线程的优先级
t.setPriority(Thread.MAX_PRIORITY);
//获得线程的优先级
System.out.println(t.getPriority());

线程优先级低并不是说该线程就不会被CPU执行,而是说概率会降低

4. 守护线程

线程分为守护线程和用户线程,我们一般创建的线程都是用户线程,如果想将一个线程设置为守护线程,那么可以通过t.setDaemon(true);来设置。

Java虚拟机不会考虑守护线程何时结束,只要用户线程运行完毕,守护线程就直接被摧毁。

二、线程安全问题

1. 问题引入

对于线程安全问题,我们可以先了解一个简单也是比较经典的案例:售票问题

static class MyTask implements Runnable{
    private int num = 100;
    @Override
    public void run() {
        saleTicket();
    }

    private void saleTicket(){
        while (true){
              if (num <= 0) break;
              System.out.println(Thread.currentThread().getName() + ":::" + num--);
              sleep();
        }
    }
    private void sleep(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

该类用于模拟售票,里面有一个成员变量numsaleTicket方法用来模拟售票,如果有人来买票那么num就减一

public static void main(String[] args) {
    MyTask task = new MyTask();
    Thread t1 = new Thread(task);
    Thread t2 = new Thread(task);
    Thread t3 = new Thread(task);
    Thread t4 = new Thread(task);
    t1.start();
    t2.start();
    t3.start();
    t4.start();
}

主方法内部创建了四个线程,每个线程传入的任务相同,模拟四个买票的窗口。

在这里插入图片描述

从结果可以看出,数据出现了问题。

2. 多线程中的安全问题

多线程中的安全问题主要是涉及到公共数据的安全性问题。如果多个线程同时共享一个变量,对同一个变量进行修改,那么就有可能出现线程安全问题。例如说售票问题,当多个线程同时访问num时就导致了num数据不准确的问题。

当一个线程进入while循环但还没来的及进行输出就失去了CPU的控制权这时num的值可能已经修改过了但是没有打印出来,但是当下一个线程进入while循环时if中判断的num值没有变,但是当它再进行修改时可能num已经小于0了,这时就会出现问题。

在这里插入图片描述

在这里插入图片描述

3. synchronized

为了保证线程安全,我们可以使用synchronized关键字来保证原子操作,synchronized锁通常被称为排他锁或者独占锁,也就是说在一个线程得到锁后其它线程只能处于阻塞状态。需要注意的是synchronized关键字在使用时要保证锁对象是一样的,否则将没有效果。

private void saleTicket() {
    while (true) {
        synchronized (this) {
            if (num <= 0) break;
            System.out.println(Thread.currentThread().getName() + ":::" + num--);
            sleep();
        }
    }
}

synchronized关键字为操作加锁,保证了线程的安全。保证线程安全关键在于保证你所做的操作为原子操作,也就是说这个操作结束后线程才会失去CPU的控制权。

如果我们这样操作

private synchronized  void saleTicket() {
    while (true) {
        if (num <= 0) break;
        System.out.println(Thread.currentThread().getName() + ":::" + num--);
        sleep();
    }
}

那么整个while循环就是一个原子操作,就会导致一个线程做全部事情,这就失去了多线程操作的意义。所以我们要在合适的位置加锁。

4. 原子性操作

我们首先看一个案例,实现一个计时器

static class Counter {
    private int count;

    public void addCount() {
        for (int i = 0; i < 10; i++) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

测试方法

public static void main(String[] args) {
    Counter counter = new Counter();
    for (int i = 0; i < 100; i++) {
        new Thread(counter::addCount).start();
    }
    while (Thread.activeCount() > 2) Thread.yield();
    System.out.println(counter.getCount());
}

通过上面的测试,我们可以发现多次运行后数据可能会出现错误,这是怎么回事呢?貌似我们的方法里只有一个count++的操作,没有多余的操作了。但是实际上我们从上一节可以看到,在底层实现时也是有很多步骤的,这些步骤共同构成了一个原子操作。那么我们该如何解决呢?只需要为一个原子操作内的代码加锁即可。

public void addCount() {
    for (int i = 0; i < 10; i++) {
        synchronized (this) {
            count++;
        }
    }
}

通常情况下,这样做就已经可以保证线程安全了,但是实际上它仍然有可能会出现错误,因为底层在优化时可能调整底层的执行顺序,顺序乱了以后还是无法保证线程的安全。为了解决这个问题,我们可以在变量前加volatile关键字。

private volatile int count;

5. 使用Lock保证线程安全

static class Counter {
    private volatile int count;
    private  Lock lock = new ReentrantLock(true);
    public void addCount() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                count++;
            }finally {
                lock.unlock();
            }
        }
    }

    public int getCount() {
        return count;
    }
}

Lock lock = new ReentrantLock(true);当参数为true时说明该锁为公平锁,否则为非公平锁

lock.lock()用来为代码块加锁

lock.unlock()用来为代码块解锁

我们使用的synchronized是排它锁,独占锁,非公平锁,也就是说当某一个线程拿到锁之后其它线程在未拿到锁之前是无法进入代码块的。这样可能会导致优先级低的线程没有机会执行。而Lock允许我们自己加锁,这样更灵活,同时如果设置为公平锁我们还可以保证让每个线程都有机会运行。

6. Servlet中的线程安全问题

我们知道Servlet是线程不安全的。Servlet是单例的,所有的线程公用同一个Servlet,如果我们在Servlet中使用了共享变量就有可能出现线程安全问题。为了解决这个问题我们可以使用加锁的方式,但是synchronized是排它锁,它无法保证所有线程都能执行。所以在这种情况下我们可以使用ThreadLocal为每一个线程复制一个本地变量并将这个变量和当前线程绑定。

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

三、synchronized关键字分析

synchronized的两个关键特性:互斥和可重入

如何理解可重入呢?我们先看一个例子:

public synchronized void test01(){
    test02();
}
public synchronized  void test02(){
    
}

当一个线程拿到方法锁后进入代码块执行代码,在代码块内部又调用了另一个加相同锁的方法,这时该线程依然可以进入该加锁方法。

那么我们又该如何理解互斥性呢?

这个理解起来比较简单,互斥性就是当一个线程拿到锁之后如果没有完成原子操作另一个线程就无法再次拿到锁

在这里插入图片描述

class Ticket implements Runnable{
    private volatile int num = 100;

    @Override
    public void run() {
        while (true){
            synchronized (this) {
                if (num <= 0) break;
                saleTicket();
            }
            //1.在这里放置sleep方法
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized void saleTicket(){
        System.out.println(Thread.currentThread().getName()+":::"+num--);
        //2.在这里放置sleep方法
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面的代码分别在1、2两个位置放置了sleep方法。我们可以分析一下:

如果放置在1位置,没有放置在synchronized中,那么它的效果是正常的。但是如果放在2位置,相当于在synchronized中放置sleep方法,假如说有一个线程0进入了synchronized中得到了锁然后执行了sleep方法,那么它就会失去CPU的执行权,但是它并没有失去锁,也就是说其它的线程此时可以得到CPU的执行权但是无法得到锁依然无法运行代码块,只有等线程0休眠结束并执行完原子操作失去锁后才能继续执行。

四、synchronized关键字应用分析

1. 锁一致性

class Ticket02 implements Runnable{
    private int num = 2000;
    public boolean flag = true;
    private final Object obj = new Object();
    @Override
    public void run() {
        if (flag){
            while (true){
                synchronized (obj) {
                    if (num > 0) {
                        System.out.println(Thread.currentThread().getName() + "obj--->" + num--);
                    }else break;
                }
            }
        }else {
            while (true){
                show();
            }
        }
    }
    private synchronized void show(){
        if (num >0){
            //失去执行权
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //num=1失去执行权
            System.out.println(Thread.currentThread().getName()+"fun--->"+num--);
        }
    }
}
public class ThreadTest06 {
    public static void main(String[] args) throws InterruptedException {
        Ticket02 ticket = new Ticket02();
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        t1.start();
        Thread.sleep(1);
        ticket.flag = false;
        t2.start();
    }
}

上述案例用来模拟卖票,其中flag属于标记

t1.start();
Thread.sleep(1);
ticket.flag = false;
t2.start();

在主线程中,我们将它置为false,这样t1t2两个线程就会在不同的逻辑中执行,如果flag为true那么线程执行在同步代码块中,否则执行在同步函数中,其中同步代码块的锁为Object

在理想状态下,这个程序运行起来没有什么问题。但是我们可以在show()方法中加入Thread.sleep(1);加入之后我们发现代码出现了错误,num竟然可以为0。

针对这一现象我们可以做一个分析:

首先t1线程在执行时flagtrue,所以它执行在同步代码块中。t2线程执行时flagfalse,所以它执行在同步函数中。当t1线程在某一时刻失去CPU执行权时t2线程得到执行权,但是因为Thread.sleep(1);t2线程随即进入计时等待(阻塞)状态,失去CPU执行权。如果同步函数和同步代码块使用的锁为同一个的话,即使t1线程得到了执行权但是没有锁它也无法执行。但是在上面的例子中同步函数和同步代码块所持锁并不一致,这就导致虽然使用了同步却没有效果的现象。那么为什么会出现0呢?

我们假设,当t2的到执行权时num=1,然而t2还没有执行输出就阻塞在了Thread.sleep(),此时t1再次得到执行权,如果锁一致,t1即使得到执行权因为没有得到锁依然是无法执行的,但是现在因为锁不一致,t1就会继续执行。t1执行完成以后num变为0,而这时t2又得到了执行权但此时num的值已经是0了,所以同步函数中输出的就是0

通过上述分析可以看出,要想做到同步必须保持锁一致才行,如果锁不一致和没加锁效果一样。

2、单例模式

2.1 单例模式回顾

单例模式有两种实现方式,一种被称为饿汉式,在程序运行时就立即加载。另一种称为懒汉式,只有在需要时才加载。

2.1.1 饿汉式
class Single{
    private final static Single single = new Single();
    public Single getInstant(){
        return single;
    }
}

从形式上可以看出,饿汉式比较简单明了,而且不涉及多线程并发问题。

2.1.2 懒汉式
class Single{
    private  static Single single = null;
    public Single getInstant(){
        if (single == null){
            single = new Single();
        }
        return single;
    }
}

从形式上看,懒汉式比较复杂,而且涉及到了多线程并发访问的问题。因为在判断以及创建过程中涉及到了多条操作,这些操作之间关系密切,应该是一个原子操作,如果不使用同步就可能出现同步问题。

2.2 单例模式中的同步问题

为了解决并发访问问题,我们可以直接了当的使用同步函数

class Single{
    private  static Single single = null;
    public static synchronized  Single getInstant(){
        if (single == null){
            single = new Single();
        }
        return single;
    }
}

但是,单例的并发访问问题一般只在创建对象的时候出现,而加了同步锁后每次访问这个函数就得检查锁,效率会比较的低。

class Single{
    private  static Single single = null;
    public static synchronized  Single getInstant(){
        if (single == null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
        return single;
    }
}

为了解决这个问题,我们可以使用上面的方式来进行同步。这种同步的方式确保只对创建对象的过程进行同步,解决了效率问题。

五、死锁分析

1、概述

死锁之所以会发生是因为同步锁发生嵌套,当一个线程获取锁不释放而另一个锁也需要这个锁的时候就会出现死锁现象。

学习操作系统时,给出死锁的定义为两个或两个以上的线程在执行过程中,由于竞争资源而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。简化一点说就是:一组相互竞争资源的线程因为互相等待,导致“永久”阻塞的现象

class thisTask implements Runnable{
    private boolean flag;
    public thisTask(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag){
            while (true){
                synchronized (MyLock.lock01){
                    System.out.println("if ----> lock01---->"+Thread.currentThread().getName());
                    //
                    synchronized (MyLock.lock02){
                        System.out.println("if ----> lock02---->"+Thread.currentThread().getName());
                    }
                }
            }
        }else{
            while (true){
                synchronized (MyLock.lock02){
                    System.out.println("else ----> lock02---->"+Thread.currentThread().getName());
                    //
                    synchronized (MyLock.lock01){
                        System.out.println("else ----> lock01---->"+Thread.currentThread().getName());
                    }
                }
            }
        }
    }
}
class MyLock{
    public static final MyLock lock01 = new MyLock();
    public static final MyLock lock02 = new MyLock();
}
public class DeadLockTest {
    public static void main(String[] args) {
        thisTask task01 = new thisTask(true);
        thisTask task02 = new thisTask(false);
        Thread t1 = new Thread(task01);
        Thread t2 = new Thread(task02);
        t1.start();
        t2.start();
    }
}

2、原因分析

上面是一个死锁的案例。针对上面的案例我们可以简单分析一下死锁发生的原因:

当线程t1启动时进入if代码块中执行并得到了lock01的锁,同时t2线程也启动并进入else代码块中执行并得到lock02锁。这时就会出现这样的场景:

t1线程希望得到lock02锁,t2希望得到lock01锁。但是由于未完成原子操作,两个线程都无法释放自己所持有的锁,这时就会出现死锁。但是并不是一定会出现死锁,比如说线程t1顺利得到了lock02锁,而这时线程t2恰巧也得到lock01锁,这时就会进入和谐状态。

六、生产者消费者模式与多线程

1、一个生产者和一个消费者

class Person02 {
    private String name;
    private String sex;
    private boolean flag = false;
    public synchronized void set(String name, String sex) {
        if (flag) {
            try {
                this.wait();
                System.out.println("set ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            this.name = name;
            this.sex = sex;
            flag = true;
            this.notify();
        }
    }
    public synchronized void out() {
        if (!flag) {
            try {
               this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(name + "........." + sex);
            flag = false;
            this.notify();
        }
    }
}
//生产者
class Input02 implements Runnable {
    private  Person02 person;
    Input02(Person02 person) {
        this.person = person;
    }
    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x == 0) {
                person.set("mike", "nan");
            } else {
                person.set("丽丽","女女女女女女女女女");
            }
            x = (x + 1) % 2;
        }
    }
}
//消费者
class Output02 implements Runnable {
    private Person02 person;

    Output02(Person02 person) {
        this.person = person;
    }
    @Override
    public void run() {
        while (true) {
            person.out();
        }
    }
}
public class ThreadTest08 {
    public static void main(String[] args) {
        Person02 person = new Person02();
        Input02 input = new Input02(person);
        Output02 output = new Output02(person);
        Thread t1 = new Thread(input);
        Thread t2 = new Thread(output);
        t1.start();
        t2.start();
    }
}

1.1 等待唤醒机制

多个线程在处理同一个资源时,叫同步。多个线程可处理的动作相同,如:多个线程都执行买票操作,对票资源减少。

如果处理的动作不同,通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

1.2 代码简单分析

这里有两个线程,一个用来生产(在代码中表现为切换name属性的值),一个负责消费(不断输出name属性的值)。两个线程操作同一个资源,如果不加同步会出现线程安全问题(某一个线程可能在未完成一个原子操作的时候就失去了执行权而另一个线程就直接得到执行权,这时打印的值可能就是错误的)。加锁之后可以解决线程安全问题,但是打印结果往往是一次就打印n多个相同的。但是我们希望出现交错打印的效果。为了达到这一效果我们就可以使用等待唤醒机制。

在上述代码中,我们使用了notifywait方法。

2、多个生产者和多个消费者

下面这个案例是存在问题的多生产者和多消费者。

class Resource03{
    private int count;
    private boolean flag = false;
    public synchronized void set(String name) {
        if (flag){ //问题一,在这时有可能两个线程都进入等待,因为它只进行一次判断。假如0线程执行完成之后通过该判断进入wait,而另一个线程也进入了等待状态。当被唤醒时,这两个线程无论符不符合flag的条件都不会在进行判断,而是直接向下执行
        //问题三,如果在这使用while而在下面使用notify,那么可能会出现全部处于等待状态的现象,例如0,1线程循环判断都不符合条件,一直wait。而2,3线程循环判断也不符合条件,也都处于等待状态,这样四个线程就都处于等待状态了。
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(name+"---->生产"+(++count));
        this.flag = true;
        this.notify(); //问题2,使用notify只能随机唤醒一个线程,可能会出现只唤醒自己这边的线程而没有唤醒对方线程的情况。
    }
    public synchronized void out(){
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"--------------->消费"+count);
        flag = false;
        this.notify();
    }
}
class In03 implements Runnable{
    private Resource03 resource;
    public In03(Resource03 resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
            resource.set(Thread.currentThread().getName());
        }
    }
}
class Out03 implements Runnable{
    private Resource03 resource;
    public Out03(Resource03 resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
           resource.out();
        }
    }
}
public class ThreadTest11 {
    public static void main(String[] args) {
        Resource02 resource = new Resource02();
        In02 in = new In02(resource);
        Out02 out = new Out02(resource);
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        Thread t3 = new Thread(in);
        Thread t4 = new Thread(out);
        t1.start();
        t3.start();
        t2.start();
        t4.start();
    }
}

问题一:

if语句那里有可能两个线程都进入等待,因为if只进行一次判断。假如0线程执行完成之后通过该判断进入wait,而另一个线程也进入了等待状态。当被唤醒时,这两个线程无论符不符合flag的条件都不会在进行判断,而是直接向下执行。

问题二:

使用notify只能随机唤醒一个线程,可能会出现只唤醒自己这边的线程而没有唤醒对方线程的情况。

问题三:

如果不解决问题二而直接解决问题一(将if修改为while),那么就会存在问题三。

如果在这使用while而在下面使用notify,那么可能会出现全部处于等待状态的现象,例如0,1线程循环判断都不符合条件,一直wait。而2,3线程循环判断也不符合条件,也都处于等待状态,这样四个线程就都处于等待状态了。

下面的程序为正确的多生产者多消费者线程模型

class Resource02{
    private int count;
    private boolean flag = false;
    public synchronized void set(String name) {
        while (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(name+"---->生产"+(++count));
        this.flag = true;
        this.notifyAll();
    }
    public synchronized void out(){
        while (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"--------------->消费"+count);
        flag = false;
        this.notifyAll();
    }
}

class In02 implements Runnable{
    private Resource02 resource;
    public In02(Resource02 resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
            resource.set(Thread.currentThread().getName());
        }
    }
}

class Out02 implements Runnable{
    private Resource02 resource;

    public Out02(Resource02 resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true){
           resource.out();
        }
    }
}

public class ThreadTest10 {

    public static void main(String[] args) {
        Resource02 resource = new Resource02();
        In02 in = new In02(resource);
        Out02 out = new Out02(resource);
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        Thread t3 = new Thread(in);
        Thread t4 = new Thread(out);
        t1.start();
        t3.start();
        t2.start();
        t4.start();
    }
}

在这里插入图片描述

原文地址:https://www.cnblogs.com/zwscode/p/14284077.html