请重新认识线程

线程状态

java.lang.Thread.State 明确定义了java线程中的状态可以看到线程状态枚举有定义了六种状态代码如下

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

新建状态-NEW

new一个线程但是还没有调用start方法

Thread thread = new Thread();
System.out.println(thread.getState().name()); // 打印结果为NEW

运行状态-RUNNABLE

已调用start方法等待CPU调度或已被CPU调度

Thread thread = new Thread();
thread.start();
System.out.println(thread.getState().name()); // 打印结果为RUNNABLE

阻塞状态-BLOCKED

处于同步方法中被阻塞,等待监视器锁定的线程状态

synchronized (Runtime.class){
    Thread thread = new Thread(() -> {
        synchronized (Runtime.class){ }
    });
    thread.start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(thread.getState().name()); // 打印结果为BLOCKED
}

等待状态-WAITING

调用了以下方法,该线程将进入等待状态。进入该状态以后必须由其他线程通知唤醒

Object.wait();
thread.join();
LockSupport.park();

Object o = new Object();
Thread thread = new Thread(() -> {
    synchronized (o) {
        try {
            o.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();
try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}
synchronized (o){
    System.out.println(thread.getState().name()); // 打印结果为WAITING
    o.notify();
}

定时等待状态-TIMED_WAITING

调用了以下方法,该线程将进入超时等待状态。进入该状态以后可以由其他线程唤醒也可以等到过了超时时间自醒

Object.wait(100);
thread.join(100);
Thread.sleep(100);
LockSupport.parkNanos(100);
LockSupport.parkUntil(100);

Object o = new Object();
Thread thread = new Thread(() -> {
    synchronized (o) {
        try {
            o.wait(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();
try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

synchronized (o){
    System.out.println(thread.getState().name()); // 打印结果为TIMED_WAITING
    o.notify();
}

终止状态-TERMINATED

线程执行结束或发生异常终止执行

Thread thread = new Thread(() -> {});
thread.start();
try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(thread.getState().name()); // 打印结果为TERMINATED

状态切换流程图

请重新认识线程_第1张图片

线程中止

不正确的线程中止-Stop

Stop: 中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议使用

Destroy: JDK未实现该方法,不必对他有什么考虑

为什么说Stop会导致线程安全问题,是因为Stop中止线程的方式是从中间拦腰折断,线程无法保证执行的原子性

示例

public static void main(String[] args) {
    AtomicInteger a = new AtomicInteger();
    AtomicInteger b = new AtomicInteger();
    Thread thread = new Thread(() -> {
        synchronized (Demo.class){
            a.getAndIncrement();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            b.getAndIncrement();
        }
    });
    thread.start();
    try {
        TimeUnit.SECONDS.sleep(1);
        thread.stop();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(a.intValue() + "================" + b.intValue());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

请重新认识线程_第2张图片

按照程序正常执行的结果,我们希望线程执行一次 a和b都会同时加1,即使线程中止也要保证操作的原子性可是stop的方式没有办法让开发者保证原子性,所以这是一个不正确的中止线程方式

正确的线程中止-interrupt

如果目标线程正在调用(等待/挂起/阻塞)方法时被阻塞,那么interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常

如果目标线程是被IO或者NIO中的Channel所阻塞,同样IO操作会被中断或者返回特殊异常值.达到终止线程的目的

示例

把stop改为interrupt即可

public static void main(String[] args) {
    AtomicInteger a = new AtomicInteger();
    AtomicInteger b = new AtomicInteger();
    Thread thread = new Thread(() -> {
        synchronized (Demo.class){
            a.getAndIncrement();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            b.getAndIncrement();
        }
    });
    thread.start();
    try {
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(a.intValue() + "================" + b.intValue());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

请重新认识线程_第3张图片

可以看到抛出了一个InterruptedException异常,但是程序的执行结果正确也就是所见既所得,不像stop一样强行中止了线程。也就是如果用interrupt的方式中止线程,会通过抛出一个异常通知目标线程,由开发者控制收到异常之后的执行逻辑让整个程序依然处于线程安全状态

正确的线程中止-标志位

如果是一种循环执行的逻辑可以通过增加标志位去控制程序是否继续执行,受限于需要有循环执行的逻辑

示例

/**
 * @author pangbohuan
 * @date 2020/5/19 0019 16:42
 * @description
 */
public class Demo {

    private volatile static boolean flag = true;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (flag) {
                System.out.println("循环执行");
            }
        });
        thread.start();

        try {
            TimeUnit.SECONDS.sleep(1);
            // 可以通过改变flag的值关闭线程
            flag = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

CPU优化手段和内存屏障

CPU高速缓存

为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。

例如:CPU高速缓存.尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能

请重新认识线程_第4张图片

L1 (一级缓存) 是CPU第一层高速缓存,分为数据缓存和指令缓存.一般服务器CPU的L1缓存的容量通常在32-4096KB

L2 (二级缓存) 由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置高速存储器,既二级缓存

L3 (三级缓存) 现在的都是内置的.而它实际上的作用即是,L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能.具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度.一般是多核共享一个L3缓存

缓存同步协议

多CPU读取同样的数据进行缓存,进行不同运算之后.最终写入主内存以哪个CPU为准?在这种高速缓存回写的场景下,有一个缓存一致性协议。多个CPU厂商对它进行了实现

MESI协议,它规定每条缓存有个状态位,同时定义了下面四个状态

状态 含义
修改态(Modified) 此cache行已被修改过(脏行),内容已不同于主存,为此cache专有
专有态(Exclusive) 此cache行内容同于主存,但不出现于其他cache中
共享态(Shared) 此cache行内容同于主存,但也出现于其他cache中
无效态(Invalid) 此cache行内容无效

多处理时,单个CPU对缓存中数据进行了改动,需要通知给其他的CPU也就意味着CPU处理要控制自己读写的操作还要监听其他CPU发出的通知从而保证最终一致

CPU运行时指令重排

指令重排序的场景:

当CPU写缓存时发现缓存区块正在被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行

示例代码

a = 1000;
b = c;

正常执行步骤

1.将1000写入a

2.读取c的值

3.将c的值写入b

重排序后执行步骤

1.读取c的值

2.将c的值写入b

3.将1000写入a

从执行结果来看正常执行步骤和重排序后执行步骤都是一样的

as-if-serial语义

重排序并非随便重排,需要遵守as-if-serial语义
as-if-serial语义的意思是指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵循as-if-serial语义

也就是说:编译器和处理器不会对存在数据依赖关系的操作做重排序

内存屏障

CPU优化后带来的问题

CPU优化后的性能确实提升了很多,但是仍然带来了两个问题

CPU高速缓存数据没有实时同步

缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的

指令重排序导致程序乱序执行

虽然遵循了as-if-serial语义,但仅在单CPU自己执行的情况下能保证结果正确。多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。

应对手段

处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述两个问题

写内存屏障(Store Memory Barrier)

在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排

读内存屏障(Load Memory Barrier)

在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据。强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题.

线程通讯

线程通讯指的是多个线程在运行的期间进行的一些数据交互或者说是协作,实现多个线程之间的协同如:线程执行先后顺序,获取某个线程执行的结果等等.涉及到线程间相互通讯分为以下四大类

文件共享

线程通过写入数据到文件,其他线程可以通过读取这个文件获取线程写入的数据.从而达到数据的交互

网络共享

通过Socket网络编程输入输出交互数据

变量共享

通过内存的区域声明共享变量,多个线程之间可以写入和读取

jdk提供的线程协调API

JDK中对于需要多线程协作完成某一任务的场景,提供了对应的API支持.多线程协作的经典场景是: 生产者-消费者模型(线程阻塞、线程唤醒)

协作场景

线程1去买包子,如果没有包子,则进入等待。线程2生产出包子,通知线程1继续执行

suspend/resume

使用方式

调用suspend挂起目标线程,通过resume可以恢复线程执行

public class Demo {
    private static Object baozi = null;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            while (baozi == null) {
                System.out.println("1.没有包子,进入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("3.买到包子,回家");
        });
        consumer.start();

        // 等待1秒后 生产一个包子
        Thread.sleep(1000);
        baozi = new Object();
        System.out.println("2.通知消费者去买包子");
        consumer.resume();
    }
}

请重新认识线程_第5张图片

弊端

从执行结果可以看到suspend/resume的方式可以完成线程之间的协调合作,但是这种方式很容易造成死锁,所以

被JDK弃用。所以用wait/notify和park/unpark机制对他进行替代

1.同步代码中使用不会释放锁

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            synchronized (Demo.class) {
                while (baozi == null) {
                    System.out.println("1.没有包子,进入等待");
                    Thread.currentThread().suspend();
                }
                System.out.println("3.买到包子,回家");
            }
        });
        consumer.start();
        // 等待1秒后 生产一个包子
        Thread.sleep(1000);
        synchronized (Demo.class){
            baozi = new Object();
            System.out.println("2.通知消费者去买包子");
            consumer.resume();
        }
    }
}

请重新认识线程_第6张图片

执行结果是线程1进入等待以后程序卡着不动了

原因是因为在同步代码块中调用suspend/resume线程协调的方式是不会释放同步锁,导致挂起线程拿到锁挂起后通知线程再也拿不到锁

2.resume比suspend先调用

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            while (baozi == null) {
                System.out.println("1.没有包子,进入等待");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Thread.currentThread().suspend();
            }
            System.out.println("3.买到包子,回家");

        });
        consumer.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        baozi = new Object();
        System.out.println("2.通知消费者去买包子");
        consumer.resume();
    }
}

请重新认识线程_第7张图片

执行结果是主线程通知线程1以后程序卡着不动了

原因是因为主线程调用resume之后线程1才调用suspend进入挂起状态,挂起后没有收到其他线程通知,导致死锁

wait/notify

这种方式是基于监视器的,只能由同一对象锁的持有者调用,也就是写在同步代码块里面,否则会抛出IllegalMonitorStateException异常会自动释放锁

使用方式

wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁

notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;
    private static Object syn = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            synchronized (syn) {
                while (baozi == null) {
                    try {
                        System.out.println("1.没有包子,进入等待");
                        syn.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("3.买到包子,回家");
            }
        });
        consumer.start();
        Thread.sleep(200);

        synchronized (syn){
            baozi = new Object();
            System.out.println("2.通知消费者去买包子");
            syn.notify();
        }
    }
}

请重新认识线程_第8张图片

弊端

1.notify比wait先调用

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;
    private static Object syn = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            while (baozi == null) {
                synchronized (syn) {
                    try {
                        System.out.println("1.没有包子,进入等待");
                        syn.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("3.买到包子,回家");
        });
        consumer.start();

        synchronized (syn) {
            Thread.sleep(200);
            baozi = new Object();
            System.out.println("2.通知消费者去买包子");
            syn.notify();
        }
    }
}

请重新认识线程_第9张图片

虽然wait会自动释放锁,但是对顺序有要求,如果调用notify之后再调用wait方法,线程会永远处于WAITING状态

park/unpark

线程调用park则等待许可,unpark方法为指定线程提供许可(permit)

不要求park和unpark方法的调用顺序

多次调用unpark之后,再调用park,线程会直接运行。但效果不会叠加,也就是说,连续多次调用park方法,第一次会拿到许可直接运行,后续调用会进入等待。可以理解为许可就是一个线程执行的标志位

使用方式
/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            while (baozi == null) {
                System.out.println("1.没有包子,进入等待");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();
            }
            System.out.println("3.买到包子,回家");
        });
        consumer.start();
        Thread.sleep(200);
        baozi = new Object();
        System.out.println("2.通知消费者去买包子");
        LockSupport.unpark(consumer);
    }
}

请重新认识线程_第10张图片

弊端

1.同步代码中使用不会释放锁

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static Object baozi = null;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            synchronized (Demo.class){
                while (baozi == null) {
                    System.out.println("1.没有包子,进入等待");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    LockSupport.park();
                }
                System.out.println("3.买到包子,回家");
            }

        });
        consumer.start();
        Thread.sleep(200);
        synchronized (Demo.class){
            baozi = new Object();
            System.out.println("2.通知消费者去买包子");
            LockSupport.unpark(consumer);
        }
    }
}

请重新认识线程_第11张图片

虽然park/unpark对顺序没有要求,但是在同步代码块中也会有挂起后不释放锁的问题

小结

从运行正常的角度去看待这三种方式,其实都一样。但是从异常情况来看待,这三种方式产生的异常行为都有所不同,但总的来说wait/notify和park/unpark 来的比resume/suspend 更加实用

伪唤醒

如果代码中用if语句来判断是否进入等待状态,是错误的!

官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没满足结束条件后退出

伪唤醒是指线程并非因为notify、notifyAll、unpark 等api调用而唤醒,是更底层原因导致的

线程封闭

概念

多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候,都要用到共享数据,所以线程封闭的概念就提出来了

数据都被封闭在各自的线程之中就不需要同步.这种通过将数据封闭在线程中而避免使用同步的技术成为线程封闭

线程封闭在java中具体的体现有ThreadLocal局部变量

ThreadLocal

ThreadLocal是java里一种特殊的变量

它是一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的变量,竞争条件就被彻底消除了,这是在并发模式下是绝对安全的变量

使用方式

ThreadLocal threadLocal = new ThreadLocal<>();

会自动在每个线程上创建一个T的副本,副本直接彼此独立,互不影响

可以用ThreadLocal存储一些参数,以便在线程中多个方法使用,用来代替方法传参的做法

可以理解为Map map = new HashMap();

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            threadLocal.set(1);
            change();
            String name = Thread.currentThread().getName();
            System.out.println(name + "调用完A方法看下值" + threadLocal.get());
        }, "线程1").start();


        new Thread(() -> {
            threadLocal.set(2);
            change();
            String name = Thread.currentThread().getName();
            System.out.println(name + "调用完A方法看下值" + threadLocal.get());
        }, "线程2").start();
    }

    public static void change() {
        String name = Thread.currentThread().getName();
        Integer val = threadLocal.get();
        System.out.println(name + "调用A方法" + val);
        threadLocal.set(val * 100);
    }
}

请重新认识线程_第12张图片

可以看到每个线程从threadLocal 里面拿到的值都是属于这个线程的,并且可以在任意读取。

栈封闭

局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈

/**
 * @author pangbohuan
 * @date 2020/5/20 0020 9:39
 * @description
 */
public class Demo {

    public static void main(String[] args) {
        new Thread(() -> hello(), "线程1").start();
        new Thread(() -> hello(), "线程2").start();
        new Thread(() -> hello(), "线程3").start();
    }

    public static void hello() {
        String hello = "hello world!";
        System.out.println(Thread.currentThread().getName() + "说" + hello);
        hello = "你好世界";
    }
}

请重新认识线程_第13张图片

从执行结果来看无论有多少线程调用hello方法即使改变方法里面变量的值,线程都不会相互影响,这就是栈封闭的体现。

你可能感兴趣的:(java,多线程,并发编程)