java多线程学习总结
在工作之余发现多线程对于自己来说比较重要,结合工作重新对多线程进行学习梳理。
一多线程的基本概念总结
1.进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。
2.线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。
3.多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不 是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
1 package test; 2 3 import dao.User; 4 5 /** 6 * Created by guoYunLong on 2017/5/30 0030. 7 */ 8 public class news { 9 void transferMoney(User from, User to, float amount){ 10 to.setMoney(to.getBalance() + amount); 11 from.setMoney(from.getBalance() - amount); 12 } 13 }
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
4.线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见
可见性问题:
a、CPU采用时间片轮转等不同算法来对线程进行调度对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。
b、CPU缓存:
目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。
c、指令顺序重排
出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。
二基本线程类
基本线程类指的是Thread类,Runnable接口,Callable接口
Thread 类实现了Runnable接口,启动一个线程的方法:
a.继承Thread类:
1 package test; 2 3 import dao.User; 4 5 /** 6 * Created by guoYunLong on 2017/5/30 0030. 7 */ 8 9 class Thread1 extends Thread{ 10 private String name; 11 public Thread1(String name) { 12 this.name=name; 13 } 14 public void run() { 15 for (int i = 0; i < 5; i++) { 16 System.out.println(name + "运行 : " + i); 17 try { 18 sleep((int) Math.random() * 10); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 } 23 24 } 25 26 public static void main(String[] args) { 27 Thread1 mTh1=new Thread1("A"); 28 Thread1 mTh2=new Thread1("B"); 29 mTh1.start(); 30 mTh2.start(); 31 32 } 33 34 35 }
b.实现Runnable接口:
1 package test; 2 3 4 /** 5 * Created by guoYunLong on 2017/5/30 0030. 6 */ 7 8 class Thread2 implements Runnable{ 9 private String name; 10 11 public Thread2(String name) { 12 this.name=name; 13 } 14 15 @Override 16 public void run() { 17 for (int i = 0; i < 5; i++) { 18 System.out.println(name + "运行 : " + i); 19 try { 20 Thread.sleep((int) Math.random() * 10); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 } 25 26 } 27 28 29 public static void main(String[] args) { 30 new Thread(new Thread2("C")).start(); 31 new Thread(new Thread2("D")).start(); 32 } 33 34 35 }
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
三线程状态转换

1、synchronized关键字
方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)
所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。
a、静态方法:Java类对应的Class类的对象所关联的监视器对象。
b、实例方法:当前对象实例所关联的监视器对象。
c、代码块:代码块声明中的对象所关联的监视器对象。
注:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。
例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象
1 package test; 2 3 import dao.User; 4 5 /** 6 * Created by guoYunLong on 2017/5/30 0030. 7 */ 8 9 class SynchronizedIdGenerator{ 10 private int value = 0; 11 public synchronized int getNext(){ 12 return value++; 13 } 14 public int getNextV2(){ 15 synchronized(this){ 16 return value++; 17 } 18 } 19 }
2、Object类的wait、notify和notifyAll方法
生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。
(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
工作流程:
a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
1 package test; 2 3 /** 4 * Created by guoYunLong on 2017/5/30 0030. 5 */ 6 public class new3 { 7 public synchronized String pop(){ 8 // 唤醒对象等待池中的所有线程,可能唤醒的就是 生产者 9 // (当生产者发现产品满,就会进入对象的等待池,这里代码省略,基本略同) 10 this.notifyAll(); 11 //如果发现没产品,就释放锁,进入对象等待池 12 while(index == -1){ 13 this.wait(); 14 } 15 //当生产者生产完后,消费者从this.wait()方法再开始执行, 16 // 第一次还会执行循环,万一产品还是为空,则再等待,所以这里必须用while循环,不能用if 17 String good = buffer[index]; 18 buffer[index] = null; 19 index--; 20 return good;// 消费完产品,退出。 21 } 22 }
注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。
3、线程状态转换:
已经废弃的方法:stop、suspend、resume、destroy,这些方法在实现上时不安全的。
线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING(有超时的等待)、TERMINATED。
a、方法sleep()进入的阻塞状态,不会释放对象的锁(即大家一起睡,谁也别想执行代码),所以不要让sleep方法处在synchronized方法或代码块中,否则造成其他等待获取锁的线程长时间处于等待。
b、方法join()则是主线程等待子线程完成,再往下执行。
c、方法interrupt(),向被调用的对象线程发起中断请求。如线程A通过调用线程B的d的interrupt方法来发出中断请求,线程B来处理这个请求,当然也可以忽略,这不是必须的。Object类的wait()、Thread类的join()和sleep方法都会抛出受检异常java.lang.InterruptedException,通过interrupt方法中断该线程会导致线程离开等待状态。对于wait()调用来说,线程需要重新获取监视器对象上的锁之后才能抛出InterruptedException异常,并致以异常的处理逻辑。
可以通过Thread类的isInterrupted方法来判断是否有中断请求发生,通常可以利用这个方法来判断是否退出线程(类似上面的volatitle修饰符的例子);
Thread类还有个方法Interrupted(),该方法不但可以判断当前线程是否被中断,还会清楚线程内部的中断标记,如果返回true,即曾被请求中断,同时调用完后,清除中断标记。
如果一个线程在某个对象的等待池,那么notify和interrupt 都可以使该线程从等待池中被移除。如果同时发生,那么看实际发生顺序。如果是notify先,那照常唤醒,没影响。如果是interrupt先,并且虚拟机选择让该线程中断,那么即使nofity,也会忽略该线程,而唤醒等待池中的另一个线程。
e、yield(),尝试让出所占有的CPU资源,让其他线程获取运行机会,对操作系统上的调度器来说是一个信号,不一定立即切换线程。(在实际开发中,测试阶段频繁调用yeid方法使线程切换更频繁,从而让一些多线程相关的错误更容易暴露出来)。
五高级同步对象(提高开发效率)
1、信号量。
信号量一般用来数量有限的资源,每类资源有一个对象的信号量,信号量的值表示资源的可用数量。
在使用资源时,需要从该信号量上获取许可,成功获取许可,资源的可用数-1;完成对资源的使用,释放许可,资源可用数+1; 当资源数为0时,需要获取资源的线程以阻塞的方式来等待资源,或过段时间之后再来检查资源是否可用。(上面的SimpleResourceManager类实际上时信号量的一个简单实现)
java.util.concurrent.Semaphore类,在创建Semaphore类的对象时指定资源的可用数
a、acquire(),以阻塞方式获取许可
b、tryAcquire(),以非阻塞方式获取许可
c、release(),释放许可。
d、accquireUninterruptibly(),accquire()方法获取许可以的过程可以被中断,如果不希望被中断,使用此方法。
1 package test; 2 3 import com.sun.org.apache.xml.internal.serialize.Printer; 4 5 import java.util.Collection; 6 import java.util.List; 7 import java.util.concurrent.Semaphore; 8 9 /** 10 * Created by guoYunLong on 2017/5/30 0030. 11 */ 12 public class PrinterManager { 13 private final Semphore semaphore; 14 private final List<Printer> printers = new ArrayList<>(): 15 public PrinterManager(Collection<? extends Printer> printers){ 16 this.printers.addAll(printers); 17 //这里重载方法,第二个参数为true,以公平竞争模式,防止线程饥饿 18 this.semaphore = new Semaphore(this.printers.size(),true); 19 } 20 public Printer acquirePrinter() throws InterruptedException{ 21 semaphore.acquire(); 22 return getAvailablePrinter(); 23 } 24 public void releasePrinter(Printer printer){ 25 putBackPrinter(pinter); 26 semaphore.release(); 27 } 28 private synchronized Printer getAvailablePrinter(){ 29 printer result = printers.get(0); 30 printers.remove(0); 31 return result; 32 } 33 private synchronized void putBackPrinter(Printer printer){ 34 printers.add(printer); 35 } 36 }
2、倒数闸门
多线程协作时,一个线程等待另外的线程完成任务才能继续进行。
java.util.concurrent.CountDownLatch类,创建该类时,指定等待完成的任务数;当一个任务完成,调用countDonw(),任务数-1。等待任务完成的线程通过await(),进入阻塞状态,直到任务数量为0。CountDownLatch类为一次性,一旦任务数为0,再调用await()不再阻塞当前线程,直接返回。
1 package test; 2 3 import org.apache.commons.io.IOUtils; 4 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.net.URL; 8 import java.util.Collections; 9 import java.util.Map; 10 import java.util.concurrent.CountDownLatch; 11 12 /** 13 * Created by guoYunLong on 2017/5/30 0030. 14 */ 15 public class PageSizeSorter { 16 // 并发性能远远优于HashTable的 Map实现,hashTable做任何操作都需要获得锁,同一时间只有有个线程能使用,而ConcurrentHashMap是分段加锁,不同线程访问不同的数据段,完全不受影响,忘记HashTable吧。 17 private static final ConcurrentHashMap<String , Interger> sizeMap = new ConcurrentHashMap<>(); 18 private static class GetSizeWorker implements Runnable{ 19 private final String urlString; 20 public GetSizeWorker(String urlString , CountDownLatch signal){ 21 this.urlString = urlStirng; 22 this.signal = signal; 23 } 24 public void run(){ 25 try{ 26 InputStream is = new URL(urlString).openStream(); 27 int size = IOUtils.toByteArray(is).length; 28 sizeMap.put(urlString, size); 29 }catch(IOException e){ 30 sizeMap.put(urlString, -1); 31 }finally{ 32 signal.countDown()://完成一个任务 , 任务数-1 33 } 34 } 35 } 36 private void sort(){ 37 List<Map.Entry<String, Integer> list = new ArrayList<sizeMap.entrySet()); 38 Collections.slort(list, new Comparator<Map.Entry<String, Integer>>() { 39 public int compare(Entry<String, Integer> o1, Entry<Sting, Integer> o2) { 40 return Integer.compare(o2.getValue(), o1.getValue()); 41 } 42 43 ; 44 System.out.println(Arrays.deepToString(list.toArray())); 45 } 46 47 public void sortPageSize(Collection<String> urls) throws InterruptedException{ 48 CountDownLatch sortSignal = new CountDownLatch(urls.size()); 49 for(String url: urls){ 50 new Thread(new GetSizeWorker(url, sortSignal)).start(); 51 } 52 sortSignal.await()://主线程在这里等待,任务数归0,则继续执行 53 sort(); 54 } 55 }
3、循环屏障
循环屏障在作用上类似倒数闸门,不过他不像倒数闸门是一次性的,可以循环使用。另外,线程之间是互相平等的,彼此都需要等待对方完成,当一个线程完成自己的任务之后,等待其他线程完成。当所有线程都完成任务之后,所有线程才可以继续运行。
当线程之间需要再次进行互相等待时,可以复用同一个循环屏障。
类java.uti.concurrent.CyclicBarrier用来表示循环屏障,创建时指定使用该对象的线程数目,还可以指定一个Runnable接口的对象作为每次循环后执行的动作。(当最后一个线程完成任务之后,所有线程继续执行之前,被执行。如果线程之间需要更新一些共享的内部状态,可以利用这个Runnalbe接口的对象来处理)。
每个线程任务完成之后,通过调用await方法进行等待,当所有线程都调用await方法之后,处于等待状态的线程都可以继续执行。在所有线程中,只要有一个在等待中被中断,超时或是其他错误,整个循环屏障会失败,所有等待中的其他线程抛出java.uti.concurrent.BrokenBarrierException。
例:每个线程负责找一个数字区间的质数,当所有线程完成后,如果质数数目不够,继续扩大范围查找
1 package test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import java.util.concurrent.CyclicBarrier; 6 7 /** 8 * Created by guoYunLong on 2017/5/30 0030. 9 */ 10 public class PrimeNumber { 11 private static final int TOTAL_COUTN = 5000; 12 private static final int RANGE_LENGTH= 200; 13 private static final int WORKER_NUMBER = 5; 14 private static volatitle boolean done = false; 15 private static int rangeCount = 0; 16 private static final List<Long> results = new ArrayList<Long>(): 17 private static final CyclicBarrier barrier = new CyclicBarrier(WORKER_NUMBER, new Runnable(){ 18 public void run(){ 19 if(results.size() >= TOTAL_COUNT){ 20 done = true; 21 } 22 } 23 }); 24 private static class PrimeFinder implements Runnable{ 25 public void run(){ 26 while(!done){// 整个过程在一个 while循环下,await()等待,下次循环开始,会再次判断 执行条件 27 int range = getNextRange(); 28 long start = rang * RANGE_LENGTH; 29 long end = (range + 1) * RANGE_LENGTH; 30 for(long i = start; i<end;i++){ 31 if(isPrime(i)){ 32 updateResult(i); 33 } 34 } 35 try{ 36 barrier.await(); 37 }catch (InterruptedException | BokenBarrierException e){ 38 done = true; 39 } 40 } 41 } 42 } 43 private synchronized static void updateResult(long value){ 44 results.add(value); 45 } 46 private synchronized static int getNextRange(){ 47 return rangeCount++; 48 } 49 private static boolean isPrime(long number){ 50 //找质数的代码 51 } 52 public void calculate(){ 53 for(int i=0;i<WORKER_NUMBER;i++){ 54 new Thread(new PrimeFinder()).start(); 55 } 56 while(!done){ 57 58 } 59 //计算完成 60 } 61 62 }
4、对象交换器
适合于两个线程需要进行数据交换的场景。(一个线程完成后,把结果交给另一个线程继续处理)
java.util.concurrent.Exchanger类,提供了这种对象交换能力,两个线程共享一个Exchanger类的对象,一个线程完成对数据的处理之后,调用Exchanger类的exchange()方法把处理之后的数据作为参数发送给另外一个线程。而exchange方法的返回结果是另外一个线程锁提供的相同类型的对象。如果另外一个线程未完成对数据的处理,那么exchange()会使当前线程进入等待状态,直到另外一个线程也调用了exchange方法来进行数据交换。
1 package test; 2 3 import java.util.concurrent.Exchanger; 4 5 /** 6 * Created by guoYunLong on 2017/5/30 0030. 7 */ 8 public class new3 { 9 private final Exchanger<StringBuilder> exchanger = new Exchanger<StringBuilder>(); 10 private class Sender implements Runnable{ 11 public void run(){ 12 try{ 13 StringBuilder content = new StringBuilder("Hello"); 14 content = exchanger.exchange(content); 15 }catch(InterruptedException e){ 16 Thread.currentThread().interrupt(); 17 } 18 } 19 } 20 private class Receiver implements Runnable{ 21 public void run(){ 22 try{ 23 StringBuilder content = new StringBuilder("World"); 24 content = exchanger.exchange(content); 25 }catch(InterruptedException e){ 26 Thread.currentThread().interrupt(); 27 } 28 } 29 } 30 public void exchange(){ 31 new Thread(new Sender()).start(); 32 new Thread(new Receiver()).start(); 33 } 34 }
六高级多线程控制类
1.ThreadLocal类
java.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。
a、get()和set()分别用来获取和设置当前线程中包含的对象的值。
b、remove(),删除。
c、initialValue(),初始化值。如果没有通过set方法设置值,第一个调用get,会通过initValue来获取对象的初始值。
ThreadLoacl的一般用法,创建一个ThreadLocal的匿名子类并覆盖initalValue(),把ThreadLoacl的使用封装在另一个类中
1 package test; 2 3 4 /** 5 * Created by guoYunLong on 2017/5/30 0030. 6 */ 7 public class new1 { 8 private static final ThreadLocal<IdGenerator> idGenerator = new ThreadLocal<IdGenerator>() { 9 protected IdGenerator initalValue() { 10 return new IdGenerator();//IdGenerator 是个初始int value =0,然后getNext(){ return value++} 11 } 12 }; 13 14 public static int getNext() { 15 return idGenerator.get().getNext(); 16 } 17 18 19 public static void main(String[] args) { 20 new Thread(new Thread2("C")).start(); 21 new Thread(new Thread2("D")).start(); 22 } 23 24 }
ThreadLoal的另外一个作用是创建线程唯一的对象,在有些情况,一个对象在代码中各个部分都需要用到,传统做法是把这个对象作为参数在代码间传递,如果使用这个对I昂的代码都在同一个线程,可以封装在ThreadLocal中。如:在多线程中,生成随机数java.util.Random会带来竞争问题,java.util.concurrent.ThreadLocalRandom类提供多线程下的随机数声场,底层是ThreadLoacl。
2.原子类(AtomicInteger、AtomicBoolean……)
如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized
1 //返回值为boolean 2 AtomicInteger.compareAndSet(int expect,int update)
AtomicReference
对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号
3.Lock类
lock: 在java.util.concurrent包内。共有三个实现:
1 ReentrantLock 2 ReentrantReadWriteLock.ReadLock 3 ReentrantReadWriteLock.WriteLock
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。
区别如下:lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)提供多种加锁方案,lock 阻塞式, trylock 无阻塞式,
lockInterruptily 可打断式, 还有trylock的带超时时间版本。本质上和监视器锁(即synchronized是一样的)能力越大,责任越大,必须控制好加锁和解锁,
否则会导致灾难。和Condition类的结合。性能更高,对比如下图:

synchronized和Lock性能对比
七最后总结
总结:因为时间匆忙,纯属学习,自己花的时间不是很多,感觉来说可以这么考虑:多线程开发中应该优先使用高层API,如果无法满足,使用java.util.concurrent.atomic和java.util.concurrent.locks包提供的中层API,而synchronized和volatile,以及wait,notify和notifyAll等低层API 应该最后考虑。

浙公网安备 33010602011771号