一、多线程用途
-
提高运行效率(多核设备情况下),一般来说单核设备下的多线程属于假的多线程,但是在多核情况下,多线程能大大提高效率,充分的利用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();