java 多线程

317 阅读12分钟

一、多线程用途

  • 提高运行效率(多核设备情况下),一般来说单核设备下的多线程属于假的多线程,但是在多核情况下,多线程能大大提高效率,充分的利用cpu

  • 防止阻塞 多线程能异步处理一些耗时任务,防止阻塞,比如要请求其他服务获取一些数据,但是后续有些东西又不是依赖返回的数据,所以这里可以使用多线程进行异步处理。

二、多线程创建

  • 继承Thread类

  • 实现Runnable接口

public class testThread implements Runnable{
    @Override
    public void run() {
        System.out.println("testThread run");
    }

    public static void main(String[] args) {
        testThread thread = new testThread();
        Thread t = new Thread(thread);
        t.start();
    }
}
  • 实现Callable 接口
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class testThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //计算逻辑
        return 1;
    }
    public static void main(String[] args) {
        testThread td = new testThread();
        FutureTask<Integer> result = new FutureTask<>(td);
        new Thread(result).start();
        try {
            Integer sum = result.get();  //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

关于 run和 start方法。start方法是启动一个线程的方法,而线程中的run方法是线程中需要执行的东西。如果直接调用run方法,会当作同步使用,所以这里一定要注意,多线程启动的方法是start 不是run。

  • ExecutorService 线程池启动,和者没有本质区别,只是在启动方式上有区别
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class TestThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //计算逻辑
        return 1;
    }
    public static void main(String[] args) {
        int taskSize = 5;
        // 创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        //也可以用 runable接口
        pool.submit(new TestThread1());
        for (int i = 0; i < taskSize-1; i++) {
            Callable c = new TestThread();
            // 执行任务并获取Future对象
            Future f = pool.submit(c);
            // System.out.println(">>>" + f.get().toString());
            list.add(f);
        }
        // 关闭线程池
        pool.shutdown();

        // 获取所有并发任务的运行结果
        for (Future f : list) {
            // 从Future对象上获取任务的返回值,并输出到控制台
            try {
                System.out.println(">>>" + f.get().toString());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }


还有通过匿名方法创建多线程并启动,这里不做多描述

三、关键字Volatile 和 synchronized

volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。 一般来说,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰,会产生下列影响:

1 线程间操作的可见性

这里要稍微提一下内存可见性


public class SynchronizedTest{

    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        status = true;
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){
            System.out.println("running....");
        }
    }
}

如上述代码,假设在一个线程中执行了 changestatus方法,另一个线程中并不一定能打印出想要的结果,原因在于可见性。 所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。   JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下

所以对于上面的代码只需将status变量加上 volatile 关键字。

但是这个关键字并不是万能的。在一些复合类操作中,会存在一些问题

mport java.util.concurrent.CountDownLatch;

public class SynchronizedTest{

        public static volatile int num = 0;
        //使用CountDownLatch来等待计算线程执行完
        static CountDownLatch countDownLatch = new CountDownLatch(30);
        public static void main(String []args) throws InterruptedException {
            //开启30个线程进行累加操作
            for(int i=0;i<30;i++){
                new Thread(){
                    public void run(){
                        for(int j=0;j<10000;j++){
                            num++;//自加操作  num = num+1;也是同理 
                            //int i= num;num = i+1; 同理
                        }
                        countDownLatch.countDown();
                    }
                }.start();
            }
            //等待计算线程执行完
            countDownLatch.await();
            System.out.println(num);
        }

}

输出: 268270

按照之前说的 使用 volatile 修饰的,理应输出 300000。这里问题就出在一个操作 num++,num++不是原子性的操作, 可以将这一步理解为 三部操作, 读取 加一 赋值,但是 这里的 读取和 加一都是在本地内存中,所以可能其他线程的+1操作已经执行了很多了,然后这里又会进行相应的覆盖,所以结果要小于预期结果。所以在java并发包中提供了一个方案针对这个操作的

 //使用原子操作类
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num.incrementAndGet();//原子性的num++,通过循环CAS方式
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }

结果: 300000

2:volatile能禁止指令重排序

一般来说,java虚拟机为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,会按照自己的规则在语义上不影响对程序的编写顺序进行一些打乱,如下面的例子:



public class SynchronizedTest extends Thread{
    /** 这是一个验证结果的变量 */
    private static int a=1;
    /** 这是一个标志位 */
    private static boolean flag=false;

    //由于多线程情况下未必会试出重排序的结论,所以多试一些次
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            ThreadA threadA=new ThreadA();
            ThreadB threadB=new ThreadB();
            threadA.start();
            threadB.start();
            //这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些.
            threadA.join();
            threadB.join();
            a=0;
            flag=false;
        }
    }
    static class ThreadA extends Thread{
        public void run(){
            a=1;
            flag=true;
        }
    }
    static class ThreadB extends Thread{
        public void run(){
            if(flag){
                a=a*1;
            }
            if(a==0){
                System.out.println("ha,a==0");
            }
        }
    }
}

打印结果:ha,a==0 或者 无打印

这里就有疑问了 a=0的赋值是在线程执行完之后,为什么还会出现 a=0的情况呢?原因就在与虚拟机的重排序,按照main方法里面的逻辑,都没有直接的对 a变量进行读写操作(没有对这个变量有依赖),所以可能对这个赋值的指令进行重排序。(并非一定出现,可以多运行几次) 但是使用 volatile 就不一样了,指明了关于这个变量相关的指令不进行重排序。

volatile原理如下: “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

下面说下synchronized关键字:

synchronized也是多线程开发中一个比较重要的关键字, 可用于修饰代码块和方法,

public class SynchronizedTest1 extends Thread{

    private SynchronizedTest synchronizedTest;
    private boolean flag;
    SynchronizedTest1(SynchronizedTest synchronizedTest, boolean flag){
      this.synchronizedTest = synchronizedTest;
      this.flag = flag;
    }
    @Override
    public void run() {
        if (flag){
            try {
                synchronizedTest.methodA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            synchronizedTest.methodB();
        }
    }
    public static void main(String[] args) {

        SynchronizedTest synchronizedTest = new SynchronizedTest();
        SynchronizedTest1 synchronizedTest1 = new SynchronizedTest1(synchronizedTest, true);
        SynchronizedTest1 synchronizedTest2 = new SynchronizedTest1(synchronizedTest, false);
        synchronizedTest1.start();
        synchronizedTest2.start();
    }
}
public class SynchronizedTest{
        // 修饰方法 如果方法是static 则锁定该类所有实例
    synchronized public void methodA() throws InterruptedException {
                 //do something....
        System.out.println("methodA");
        Thread.sleep(10000);
    }

    public void methodB() {
    // 修饰代码块
         synchronized (this) {
                //do something....
                System.out.println("methodB");
            }
    }
}

打印结果:

methodAThu Mar 07 11:27:33 CST 2019

methodBThu Mar 07 11:27:43 CST 2019

从上述例子可以看出,当synchronized修饰时,若多个线程拥有同一个MyObject类的对象,则这些方法只能以同步的方式执行。即,执行完一个synchronized修饰的方法或代码块后,才能执行另一个synchronized修饰的方法或代码块。(可以理解为锁)

3 关键词使用范例

(1)synchronized, wait, notify结合:典型场景生产者消费者问题


public class Tip1 {

    private int product;

    private static int MAX_PRODUCT = 10;

    private static int MIN_PRODUCT = 1;


    /**
     * 生产者生产出来的产品交给店员
     */
    public synchronized void produce()
    {
        if(this.product >= MAX_PRODUCT)
        {
            try
            {
                wait();
                System.out.println("产品已满,请稍候再生产");
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
            return;
        }

        this.product++;
        System.out.println("生产者生产第" + this.product + "个产品.");
        notifyAll();   //通知等待区的消费者可以取出产品了
    }

    /**
     * 消费者从店员取产品
     */
    public synchronized void consume()
    {
        if(this.product <= MIN_PRODUCT)
        {
            try
            {
                wait();
                System.out.println("缺货,稍候再取");
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            return;
        }

        System.out.println("消费者取走了第" + this.product + "个产品.");
        this.product--;
        notifyAll();   //通知等待去的生产者可以生产产品了
    }
}

(2) 单例创建

import java.util.Objects;

public class TestIns {

    private volatile static TestIns testIns;
    public static TestIns getTestIns(){

        if (Objects.isNull(testIns)) {
            synchronized (TestIns.class) {

            }
        }
        return testIns;
    }
}


需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。testIns = new TestIns();可以分解为3行伪代码 1.memory = allocate() //分配内存 2. ctorInstanc(memory) //初始化对象 3. testIns = memory //设置testIns指向刚分配的地址 上面的代码在编译运行时,可能会出现重排序从1-2-3排序为1-3-2。在多线程的情况下会出现以下问题。线程A在执行第5行代码时,B线程进来,而此时A执行了1和3,没有执行2,此时B线程判断instance不为null,直接返回一个未初始化的对象。

四、多线程相关方法

  • thread 相关
//当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
public static Thread.yield() 
//暂停一段时间
public static Thread.sleep()  
//在一个线程中调用other.join(),将等待other执行完后才继续本线程。&emsp;&emsp;&emsp;&emsp;
public join()
//后两个函数皆可以被打断
public interrupte()

关于中断
它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。 Thread.interrupted()检查当前线程是否发生中断,返回boolean synchronized在获锁的过程中是不能被中断的。
中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体

  • Callable相关

future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态

ExecutorService e = Executors.newFixedThreadPool(3);
 //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束
  • ThreadLocal

用处:保存线程的独立变量。对一个线程类(继承自Thread) 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。 主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

log4j的xml能直接读到 %X{参数名} 常用来自定义日志,微服务打印日志,做日志集中处理的时候会用到

  • AtomicInteger和AtomicBoolean AtomicReference原子类
//返回值为boolean
AtomicInteger.compareAndSet(int expect,int update)

对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。 这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号

  • Lock相关
// 共有三个实现类
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序) 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。 本质上和监视器锁(即synchronized是一样的) 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。 和Condition类的结合。 性能更高,对比如下图:

基本的线程相关的就这些了,还有些更加深入的以后继续补充

如何让多线程顺序执行 join方法

static ExecutorService executorService = Executors.newSingleThreadExecutor();