java多线程并发笔记总结(一)持续更新系列

/ 默认分类 / 0 条评论 / 895浏览

1.线程和进程

下图说明的就是jvm进程与其中的线程之间的关系:

jvm进程与其中的线程之间的关系

可以发现一个进程中可以有多个线程,比如这个jvm进程,每个线程可以共享进程中的堆和方法区,并且每一个线程也是具有自己的私有空间的,比如他们都有自己的程序计数器和方法栈

线程的几种状态

2. java的线程创建

java中的线程使用的是Thread这个类,创建一个线程有两种方式(此句话是来自jdk官方注释),事实上都是new出一个Thread对象出来.

第二个实现Runable接口也是需要实现其中的run方法,而实现run方法其实就是在实现线程逻辑

但是创建一个线程推荐使用第二种方式,因为java只允许单继承,使用第二种方式之后还可以继续继承拓展,并且这样可以使得线程和执行逻辑分离.(并且这样直接单独创建一个线程执行逻辑,这样的话,这个线程执行逻辑就可以被多个线程使用)

3. 可返回结果的线程创建

首先需要明确的一点是,这不是一种创建线程的新的方法,创建线程的方法就是上面两种方式,下面说的使用callable创建线程其实也就是修改了一下线程的执行逻辑的代码,进而使线程在执行完成之后可以返回执行完成返回的值下面我们来看一下是什么样的原理.

Q1:为什么会出现这样的callable接口来加入到线程的执行逻辑中? 在jdk1.4之前,我们的线程都是无法将执行完成之后的返回值返回的,所以在jdk1.5之后,加入了callabale这个接口,这样在线程执行完成之后就可以直接获取返回值

下面来介绍一下其中一些原理

首先来看一下callable接口,这是一个函数式接口,和runable接口是一样的,;里面就只有一个方法,但是这个call方法和run方法不一样的地方就是,run方法不能有任何的返回值并且无法抛出异常.call方法可以返回值并且可以抛出异常

@FunctionalInterface
public interface Callable<V> {

    V call() throws Exception;
}

再来看一下FutureTask这个类

//注意这个类是实现了RunableFuture接口的,其实就是间接实现了Runable接口和Future接口,所以这个类的实例也是可以作为runable的接口实现类作为创建线程时需要传入的线程执行逻辑.
public class FutureTask<V> implements RunnableFuture<V> {

        //这个属性是来存储通过构造方法传入的callable的实现类对象,
        //这样就和call方法关联起来了
        private Callable<V> callable;
        /** The result to return or exception to throw from get() */
        //这个属性是用来存储最终线程执行之后的返回值
        private Object outcome; 
        
        //构造函数来传入callable接口的实现类,进而关联上call方法
         public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
        
        //获取返回值
        public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }
    
        //最终的获取返回值回去调用的方法
        private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
    
        //将线程的返回值设置到该类的outcome属性中
        protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
        //因为最终这个类也就是作为线程的执行逻辑,并且简洁实现了Runable接口,
        //所以肯定是会有实现run方法的,并且这个run方法会比较特殊.线程的真实
        //行逻辑没有直接写到run方法中,而是写在了call方法中,然后在run方法中
        //调用,调用后会将返回值写入到outcome中
        public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

callable接口实现类

其实我们也可以直接继承Thread类创建一个子类来实现这样的返回线程执行结果,只需要在这个子类中添加一个保存线程执行逻辑的返回结果属性,之后在run方法中直接将线程执行的结果设置到这个属性中,然后在需要使用这个号结果的时候直接取出来就可以了

就像这样:

/**
 * @author CodeManZuo
 * @date 2020/6/24 - 10:42
 */
public class MyThread02<V> extends Thread {

    V outcome;

    public V getOutcome() {
        return outcome;
    }

    public void setOutcome(V outcome) {
        this.outcome = outcome;
    }

    @Override
    public void run() {

        System.out.println("线程正在执行逻辑代码,返回值已经设置到outcome,可取出");
        setOutcome((V)"我是结果");

    }
}

测试的时候注意需要等待线程执行完毕,否则取不出执行结果,futuretask其实也是异步的,get的时候会一直等到线程逻辑执行结束才会取出

    @Test
    public void test05() throws InterruptedException {
        MyThread02<String> myThread02 = new MyThread02<String>();
        myThread02.start();
        myThread02.join();
        System.out.println("现在线程已经结束了,我要取出线程的返回值");
        System.out.println("====>"+myThread02.getOutcome());
    }

4.Thread中的api

4.1 Sleep
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;

可以发现这是一个静态方法,在哪个线程中调用了该方法哪个线程就去睡觉指定的时间,并且睡觉的期间该线程也不会释放持有的monitor(后面会介绍,非常重要).

一般我们调用sleep方法去睡觉,是通过下面的方式,因为这样会比较方便直观

TimeUnit.SECONDS.sleep(10);  //这就是代表睡眠10s

某个线程中调用了sleep方法,那么这个线程会进入休眠状态,直到休眠时间达到或者被其他线程中断.并且需要注意的是,sleep方法和wait方法不一样的是,它可以不用在同步代码块中调用,也就是说调用sleep的线程不需要事先获取某个共享变量的monitor(但是如果当前线程已经获取到了某些共享变量的monitor,那么这个时候如果这个线程调用了sleep方法,那么在休眠期间也是不会释放这些monitor的,但是wait就不是这样,wait需要在同步代码块中调用,也就是说调用wait的时候当前线程肯定是已经获取了某些monitor,并且调用后会释放这些monitor)

4.2 yield
public static native void yield();

属于一种启发式的调用,用来调节cpu线程进度,调用的线程表示可以让出cpu分配的资源,但是cpu可以不理会,这决定可能需要看当前cpu资源的紧张程度.

4.3 中断线程阻塞的方式

线程中使用了wait(),sleep(),join()此类的方法后会进入阻塞状态,而调用interrupt方法就会打断阻塞.所以上面这三种方法被称为可中断方法.这些方法被中断后会抛出一个异常InterruptedException.

4.4 join

在A线程中调用B线程的join方法,A线程会被阻塞,直到B线程执行完毕

5.死锁检测

首先写一个死锁的测试程序

public class MyDeadLock {

    Object o1 = new Object();
    Object o2 = new Object();

    public void m1()
    {
        synchronized (o1)
        {
            System.out.println("m1的第一层");
            synchronized (o2)
            {
                System.out.println("m1的第二层");
            }
        }
    }
    public void m2()
    {
        synchronized (o2)
        {
            System.out.println("m2的第一层");
            synchronized (o1)
            {
                System.out.println("m2的第二层");
            }
        }
    }

    public static void main(String[] args) {
        MyDeadLock myDeadLock = new MyDeadLock();
        new Thread(() -> {
            while (true)
            {
                myDeadLock.m1();
            }
        }).start();
        new Thread(() -> {
            while (true)
            {
                myDeadLock.m2();
            }
        }).start();
    }
}

运行上面的程序,也就是会进入死锁

检测死锁的方式
方式一,使用jps和jstack 先使用jps打印出当前运行的java进程

再使用jstack查看怀疑是死锁进程所在的进程中的详细栈信息

方式二,使用jconsole工具 先打开jconsole可视化界面,选择连接疑似死锁的进程 之后点击检测死锁

6.线程间的通信

6.1 wait()

wait方法只有在获取了共享变量的monitor后才可以调用该共享变量的wait方法,那么该线程就会进入阻塞状态,只有其他线程调用了该共享变量的notify或者notifyall方法或者wait的时间到达(如果是直接调用wait空参方法那么其实底层调用的是wait(0)这个方法,0代表无限时间)这个线程才会被唤醒.

所以需要使用到wait的是共享变量,比如下面我写的这样一个生产和消费的小例子,其中队列就是共享的变量(被生产和消费者共享,所以需要我们进行一些同步的控制)

package com.cxyindex.waysofcreatethread;

import java.util.LinkedList;

/**
 * @author CodeManZuo
 * @date 2020/6/28 - 14:14
 */
public class EventQuen {

    //default maximum
    private int DEFAULT_MAXSIZE = 10;

    private int maxsize;

    private LinkedList queueList = new LinkedList();

    //use inner class as queue element
    static class Event{

    }

    EventQuen()
    {
        this.maxsize = DEFAULT_MAXSIZE;
    }

    EventQuen(int maxsize)
    {
        if(maxsize > DEFAULT_MAXSIZE || maxsize <= 0)
        {
         new EventQuen();
        }
        else
        {
            this.maxsize = maxsize;
        }
    }

    public void add(Event event) {
        synchronized (queueList)
        {
            //添加的时候如果大于最大数量了
            if(queueList.size() > maxsize)
            {
                System.out.println("队列已满,producer停止向队列添加事件");
                try {
                    queueList.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queueList.notify();
            queueList.add(event);
            System.out.println("producer添加了一个事件");
        }
    }

    public void take(){
        synchronized (queueList)
        {
         if(queueList.isEmpty())
         {
             System.out.println("队列已空,consumer停止消费队列中的事件");
             try {
                 queueList.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
         queueList.notify();
         queueList.removeFirst();
         System.out.println("consumer消费了一个事件");
        }
    }

}




  public static void main(String[] args) {
        EventQuen eventQuen = new EventQuen();

        new Thread(() -> {
            while(true)
            {
                eventQuen.add(new EventQuen.Event());
            }
        }).start();

        new Thread(() -> {
            while (true)
            {
//                try {
//                    TimeUnit.SECONDS.sleep(2);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                eventQuen.take();
            }
        }).start();
    }

运行结果:

队列已空,consumer停止消费队列中的事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
队列已满,producer停止向队列添加事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
队列已空,consumer停止消费队列中的事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
producer添加了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
consumer消费了一个事件
队列已空,consumer停止消费队列中的事件
producer添加了一个事件
producer添加了一个事件

也就是说,生产者如果发现队列已经满了就会停止生产并且停止消费者去消费;消费者如果发现队列是空的就会停止消费并且停止生产者去生产.这两个线程之间的这样的通信就是通过wait和notify实现的.

wait方法被唤醒后会继续从原来的地方执行,

6.2 notify()和notifyall()

共享变量调用其notify()方法会唤醒阻塞在这个共享变量上的线程;(这个共享变量可能会有多个线程使用了wait阻塞在这上面,notify只能唤醒一个,并且是随机的)

notifyall()可以唤醒所有由于调用该共享变量而进入阻塞的线程.

需要注意:
同步代码的monitor必须和调用wait和notify方法的共享变量是一样的,比如下面的代码


public class IllegalExample {

    private Object o1 = new Object();

    public synchronized void m1()
    {
        try {
            o1.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void m2()
    {
        o1.notify();
    }

}

创建两个线程分别执行m1和m2,那么都会报错

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.cxyindex.waysofcreatethread.IllegalExample.m1(IllegalExample.java:14)
	at com.cxyindex.waysofcreatethread.Test.main(Test.java:35)

因为调用wait和notify的前提虽然是获取了monitor,但是获取的是this也就是当前所在对象的monitor,所以会直接报错非法的monitor

7. synchronize关键字

上面其实我们已经使用过synchronize关键字了,下面我们来详细学习一下: synchronize提供的是一种互斥机制,也就是说在同一时刻只能有一个线程访问同步资源,并且,synchronize可以修饰方法和代码块,所以也就是说可以声明在某一块代码中是需要被同步的.想要进入synchronize同步代码块或者方法中就需要先获取与这个synchronize同步代码相绑定的对象的monitor才可以

下面我们通过一个例子说明一下:

public class MySyncTest {

    public final Object o = new Object();

    public void doMethod1()
    {
        synchronized (o)
        {
System.out.println("假如这里就是我们");
System.out.println("需要同步的代码区域(也就是该处的代码需要具备互斥性,同一时刻只能够被一个线程访问)");
System.out.println("那么为什么我们就将代码写在这个synchronize中就可以被同步了呢?就可以具有互斥性了呢?");
System.out.println("其实原因就是,你有没有发现,我们的synchronize无论是按照我们这样的方式来进行同步,还是说按照修饰方法或者代码块中括号中的是MySyncTest");
System.out.println("都是等于指定了一个与进入这个同步代码块 '相绑定的' monitor");
System.out.println("比如这里我们这样的写法,就是绑定了o这个对象的monitor");
System.out.println("如果是下面的doMethod02这种方式,那么就是相当于绑定的是当前这个对象");
System.out.println("如果是下面的doMethod03中的第一种方式,那么其实和doMethod02一样,绑定的是当前这个对象的monitor");
System.out.println("如果是下面domethod03中的第二种方式,那么就是绑定的这个对象所对应的class对象,所以可以同步的就是这个类中的所有的静态的方法" +
        "如果这样子定义同步代码,那么这个类中的所有的静态方法就是争抢同一个monitor");

        }
    }

    public synchronized void doMethod02()
    {

    }

    public synchronized void doMethod03()
    {
        synchronized (this)
        {

        }
        synchronized (MySyncTest.class)
        {

        }
    }
}

好了现在知道了synchronize的作用已经使用方式,我们可以从底层来了解一下,下面一起来看这样的几个问题:

Q:想进入同步代码块的线程需要先争抢到monitor,那么这到底是怎样一个过程呢?
A:每一个对象都有一个Monitor(翻译过来就是监视器),这个monitor有一个lock,我们可以叫它监视器锁,我们可以这样理解,得到这个监视器锁后就拿到了这个对象的监视器,根据synchronize的语义这样我们就可以进入同步代码块了;那么线程怎样获取对象的监视器锁呢?首先每一个监视器都会有一个计数器,当有一个线程获取到监视器锁时,计数器就加1,这个监视器锁没有被任何线程获取的时候,监视器的计数器就显示0;一个线程如果已经获取了对象的监视器锁,它还可以再次获取这个对象的监视器锁,那么计数器就继续加1,然后当这个线程需要释放监视器锁的时候就逐层释放,计数器就逐个减1,直到为0,那么这个线程就完全释放了这个对象的监视器锁,然后其他线程就可以再次获取这个对象的监视器锁了.(其实获取锁和释放锁对应的是两个指令,下面是我截取的<<java高并发详解>>书中的一段话)

明日继续更新,我们说说synchronize为什么可以实现内存可见性.....缓存一致性协议....vlotail等等