博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java 多线程
阅读量:7256 次
发布时间:2019-06-29

本文共 12068 字,大约阅读时间需要 40 分钟。

一、多线程用途

  • 提高运行效率(多核设备情况下),一般来说单核设备下的多线程属于假的多线程,但是在多核情况下,多线程能大大提高效率,充分的利用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
{ @Override public Integer call() throws Exception { //计算逻辑 return 1; } public static void main(String[] args) { testThread td = new testThread(); FutureTask
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
{ @Override public Integer call() throws Exception { //计算逻辑 return 1; } public static void main(String[] args) { int taskSize = 5; // 创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(taskSize); // 创建多个有返回值的任务 List
list = new ArrayList
(); //也可以用 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执行完后才继续本线程。    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原子类
//返回值为booleanAtomicInteger.compareAndSet(int expect,int update)复制代码

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

  • Lock相关
// 共有三个实现类ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock复制代码

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

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

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

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

static ExecutorService executorService = Executors.newSingleThreadExecutor();

转载于:https://juejin.im/post/5c7e87a46fb9a049bb7d14ba

你可能感兴趣的文章
花指令
查看>>
如何实现android蓝牙开发 自动配对连接,并不弹出提示框
查看>>
HDU更多的学校比赛9场 HDU 4965Fast Matrix Calculation【矩阵运算+数学技巧】
查看>>
两种高亮
查看>>
contentprovider的学习实例总结
查看>>
Page Scroll Effects - 简单的页面滚动效果
查看>>
【C语言的日常实践(十四)】constkeyword详细解释
查看>>
JAVA设计模式-辛格尔顿
查看>>
淘宝TFS分布式文件系统内部实现
查看>>
[ 转] 漫谈iOS Crash收集框架
查看>>
linux开机自动连接无线网络
查看>>
"http-8080-3" java.lang.OutOfMemoryError: PermGen space C3P0死锁的问题
查看>>
下拉刷新,上拉装载许多其他ListView
查看>>
Logistic回归总结
查看>>
POJ - 2828 Buy Tickets (段树单点更新)
查看>>
android layoutparams应用指南(转)
查看>>
Trie树
查看>>
12种JavaScript MVC框架之比较
查看>>
MyBatis 入门到精通(二) SQL语句映射XML文件
查看>>
C利用宏语言(#,##,do…while(0)盛大)
查看>>