java并发编程学习一
什么是进程和线程?
进程是操作系统进行资源分配的最小单位
进程跟进程之间的资源是隔离的,同一个进程之中的线程可以共享进程的资源。
线程是进程的一个实体,是CPU 调度和分派的基本单位,,依赖于进程存在。
线程无处不在:
任何一个程序都必须要创建线程,特别是Java 不管任何程序都必须启动一个
main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和Servlet、异
步消息处理机制,远程访问接口RM 等,任何一个监听事件, onclick 的触发事件等都
离不开线程和并发的知识。
多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理多线程: Simultaneous Multithreading.简称SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。
核心数、线程数:目前主流CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1 对应关系,也就是说四核CPU 一般拥有四个线程。但Intel 引入超线程技术后,使核心数与线程数形成1:2 的关系
我们平时在开发的时候,感觉并没有受cpu 核心数的限制,想启动线程就启动线程,哪怕是在单核CPU 上,为什么?这是因为操作系统提供了一种CPU 时间片轮转机制。时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
百度百科对CPU 时间片轮转机制原理解释如下:
如果在时间片结束时进程还在运行,则CPU 将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾,时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms 有用的工作之后,CPU 将花费5ms 来进行进程切换。CPU 时间的20%被浪费在了管理开销上了。为了提高CPU 效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10 个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程用足它们的时间片的话,最后一个不幸的进程不得不等待5s 才获得运行机会。多数用户无法忍受一条简短命令要5 才能
做出响应,同样的问题在一台支持多道程序的个人计算机上也会发结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU 效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms 通常是一个比较合理的折衷。
启动线程的方式有:
1、X extends Thread;,然后X.start
2、X implements Runnable;然后交给Thread 运行
区别:
Thread 才是Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)
的抽象。Thread 可以接受任意一个Runnable 的实例并执行。
中止
stop()还是interrupt() ? 判断方法: isInterrupted()和static方法interrupted()
suspend()、resume()和stop(),暂定、恢复、停止等方法不建议使用,因为这些方法都不会释放资源。
建议使用interrupt() 修改中断标示位(boolean值),当前获取到CPU运行权限的线程通过isInterrupted()或者interrupted()来判断,之后自己决定是否释放CPU权限。
注意:处于死锁状态的线程无法被中断
run()和start()
Thread 类是Java 里对线程概念的抽象,可以这样理解:我们通过new Thread()
其实只是new 出一个Thread 的实例,还没有操作系统中真正的线程挂起钩来。
只有执行了start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配cpu,分到cpu 后才调用实现
的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
而run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方
法并没有任何区别,可以重复执行,也可以被单独调用。
yield()方法:使当前线程让出CPU 占有权,但让出的时间是不可设定的,
不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线
程不一定就会持有锁,我们完全可以在释放锁后再调用yield 方法。
所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中
马上又被执行。
join()方法:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程B 中调用了线程A 的Join()方法,直到线程A 执行完毕后,才会继续
执行线程B。
线程的优先级:在Java 线程中,通过一个整型成员变量priority 来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认
优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O 操作)的线程需要设置较
高优先级,而偏重计算(需要较多CPU 时间或者偏运算)的线程则设置较低的
优先级,确保处理器不会被独占。在不同的JVM 以及操作系统上,线程规划会
存在差异,有些操作系统甚至会忽略对线程优先级的设定。
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调
度以及支持性工作。这意味着,当一个Java 虚拟机中不存在非Daemon 线程的
时候,Java 虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置
为Daemon 线程。我们一般用不上,比如垃圾回收线程就是Daemon 线程。
sleep(不会释放锁),它是线程的方法。
wait()释放锁、 notify()、notifyAll() ,Object的方法,代表着必须是在同步代码中才能执行,只有拿到锁的对象才能调用当前的方法,不然会抛异常。
wait()被唤醒之后会顺着上次执行到的地方继续执行。
随便找的一个模拟死锁的代码:
public class DeadLock implements Runnable{
private static Object obj1 = new Object();
private static Object obj2 = new Object();
private boolean flag;
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + "运行");
if(flag){
synchronized(obj1){
System.out.println(Thread.currentThread().getName() + "已经锁住obj1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj2){
// 执行不到这里
System.out.println("1秒钟后,"+Thread.currentThread().getName()
+ "锁住obj2");
}
}
}else{
synchronized(obj2){
System.out.println(Thread.currentThread().getName() + "已经锁住obj2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj1){
// 执行不到这里
System.out.println("1秒钟后,"+Thread.currentThread().getName()
+ "锁住obj1");
}
}
}
}
}
控制flag的true或者false开启多线程,导致两个线程在synchronized的状态下条件都不满足,导致死锁。
Java中线程的状态:
1、初始:创建了线程,但是还没有调用start方法。
2、运行:这里其实分为两种情况,一种就绪等待CPU,一种已经抢到CPU执行中。
3. 阻塞:线程被锁所阻塞。
4. 等待:wait,不满足对应的执行条件而等待,这里其实跟io阻塞很类似,io阻塞需要等待其他硬件执行返回。wait是自己设置了条件,可能被其他线程唤醒之后还是不满足条件,自己又进入wait,如果满足条件则进入就绪状态。
5. 超时等待:自己设置了等待时间,时间到了就可以进入就绪状态强CPU执行权。
6. 终止:线程执行完了任务。
引用一下别人画的图:状态之间的变迁如下图所示
synchronized 内置锁
synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线
程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量
访问的可见性和排他性,又称为内置锁机制。
Volatile 最轻量的同步机制
Valatile修饰的变量当发生改变时能够被其他线程感知,适合场景:一个线程写,多个线程读。
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait 方法后面的代码。
调用notify()系列方法后,对锁无影响,线程只有在syn 同步代码执行完后才
会自然而然的释放锁,所以notify()系列方法一般都是syn 同步代码的最后一行。
Synchronized和vaolatile后面会详细讲解。
线程的状态切换:
ThreadLocal
为每一个线程提供了一个变量副本,相当于每一个线程都复制了一份在本地,起到了线程的隔离,使用的都是本地变量。
使用场景Spring里面实现事物的时候实现了ThreadLocal
ThreadLocal 类接口很简单,只有4 个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK
5.0 新增的方法。需要指出的是,当线程结束后,该线程的局部变量将自动
被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected 的方法,显然是为
了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1 次调用get()
或set(Object)时才执行,并且仅执行1 次。ThreadLocal 中的缺省实现直接返回一
个null。
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
线程内部的ThreadLocal,而ThreadLocal内部维护一个
static class ThreadLocalMap {}
ThreadLocalMap 内部维护的Entry
static class Entry extends java.lang.ref.WeakReference<java.lang.ThreadLocal<?>> {
java.lang.Object value;
Entry(java.lang.ThreadLocal<?> threadLocal, java.lang.Object o) { /* compiled code */ }
}
静态的,直接类名.方法调用,set、get、remove是主要的方法
看到entry我们就知道是键值对的形式存储。
value是我们的数据源,key是以当前线程的ThreadLocal作为key,它不是静态的,而且是虚引用创建,所以在高并发的情况下如果我们对ThreadLocal的数据使用完之后不调用remove会造成内存泄漏。
每一个线程Thread里面都有一个内部类ThreadLocal,是线程的成员变量之一,而每一个ThreadLocal 内部都有一个静态类ThreadLocalMap,而ThreadLocalMap 内部又维护了一个键值对的Entry。
这个数据结构一点都不复杂,我们关注的是有可能的内存泄漏,所以我们要知道这个Entry的创建情况。
代码非常简单,我们可以看到这个ThreadLocal是虚引用创建的,所以它容易被回收,但是里面的静态ThreadLocalMap肯定是不会被回收的,Entry变量最多置空为原始状态。
ThreadLocalMap.Entry e = map.getEntry(this);
这个代码是ThreadLocal获取掉用get方法里面,如果ThreadLocal被回收了,那么这个this本身作为key就不存在了。
图中的虚线表示弱引用。
这样,当把threadlocal 变量置为null 以后,没有任何强引用指向threadlocal
实例,所以threadlocal 将会被gc 回收。这样一来,ThreadLocalMap 中就会出现
key 为null 的Entry,就没有办法访问这些key 为null 的Entry 的value,如果当前
线程再迟迟不结束的话,这些key 为null 的Entry 的value 就会一直存在一条强
引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value 永
远不会被访问到了,所以存在着内存泄露。
只有当前thread 结束以后,current thread 就不会存在栈中,强引用断开,
Current Thread、Map value 将全部被GC 回收。最好的做法是不在需要使用
ThreadLocal 变量后,都调用它的remove()方法,清除数据。
工作密取
即当前线程的Task 已经全被执行完毕,则自动取到其他线程的Task 池中取出Task 继续执行。
ForkJoinPool 中维护着多个线程(一般为CPU 核数)在不断地执行Task,每个线程除了执行自己职务内的Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高CPU 利用率。
我们要使用ForkJoin 框架,必须首先创建一个ForkJoin 任务。它提供在任务中执行fork 和join 的操作机制,通常我们不直接继承ForkjoinTask 类,只需要直接继承其子类。
- RecursiveAction,用于没有返回结果的任务
- RecursiveTask,用于有返回值的任务
task 要通过ForkJoinPool 来执行,使用submit 或invoke 提交,两者的区
别是:invoke 是同步执行,调用之后需要等待任务完成,才能执行后面的代码;
submit 是异步执行。
join()和get 方法当任务完成的时候返回计算结果。
在我们自己实现的compute 方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用invokeAll 方法时,又会进入compute 方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。
ForkJoin的使用:它是一个抽象类,我们一般实现它的子类,重写compute方法,然后将我们的业务核心写在该方法,可以设置一个阈值,当达不到这个变量的时候继续拆分,通过join方法加入到线程池中,可以理解为递归调用。当一个大的任务,可以拆分成无数个小任务,且内容核心不变的时候可以使用,当业务数量越大的时候优势越明显,例如统计SQL或者excel的数据量,百万千万数量级别的时候比较明显。
使用:
Static CountDownLatch latch = new CountDownLatch(int);在哪里创建,哪个线程需要等待就在那个线程创建,满足了条件该线程就可以继续执行。
latch.await(), 等待中。可以设置最多等待时间
例如:
if(latch != null){
try {
latch.await(20, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countdown(); //可以理解为减1,当int = 0 时候等待的线程才会继续往下执行。
创建latch,根据业务需求传入具体需要设置int数量。
需要等待的线程拿到latch 对象设置await等待。
其他线程根据业务需求当运行到业务节点的时候调用latch.countdown(),相当于int减1,当到了0的时候则等待的线程会被唤醒继续执行,或者到了最大的等待时间也会被唤醒。
注意,该计数器无法重复使用。
明白前面的CountDownLatch,当前这个CyclicBarrier就很容易理解了。
使用:
Private static CyclicBarrier barrier = new CyclicBarrier(int);
在需要等待的线程中使用barrier.await();
有几个需要等待的线程上面的int就设置几,当所有的线程都执行到barrier.await();这个代码的时候,所有等待的线程开始执行,在这之前先执行到这里的线程都必须等待。
Private static CyclicBarrier barrier = new CyclicBarrier(int,RunnablebarrierAction);
它有两个构造函数,还可以再接收一个线程,意思是当执行到当前设置的屏障barrier.await();的时候,Thread这个线程可以汇总之前等待的几个线程执行的结果,让当前这个Thread先执行完之后其他等待的线程再执行。
barrier.await();是可以反复调用,当触发当前屏障的时候就会执行一次Thread。
只支持两个线程进行Exchange,当两个线程在处理同一类数据的时候,A类型执行之后调用Exchange,B线程执行之后也调用Exchange,将需要交换的数据丢入exchange(xxx);方法中,JDK保证数据同步安全,使用场景比较狭窄。
我们都知道runable实现的接口run方法并没有返回值,当我们需要线程执行之后返回对象的时候怎么办呢? 这个时候Callable就出现了,它跟runable基本非常类似,实现它,它里面有一个call()方法,这个实现类运行你传入一个泛型,你传入的泛型是什么类型,它最后就返回什么类型的数据。
Thread里面没有任何一个构造函数能接受一个Callable,所以FutureTask就出现了。
public class FutureTask <V> implements java.util.concurrent.RunnableFuture<V> {}
public interface RunnableFuture <V> extends java.lang.Runnable, java.util.concurrent.Future<V> {
void run();
}
它实现了Runanble,所以它也可以当做一个runnable扔到Thread里面去执行,同时它又实现了Future,Future可以看做是定义了一系列辅助Callable的方法的接口。
一个FutureTask 对象可以对
调用了Callable 和Runnable 的对象进行包装,由于FutureTask 也是调用了
Runnable 接口所以它可以提交给Executor 来执行。
例如OKhttp就是。
Future里面定义什么方法来辅助呢?
public interface Future <V> {
boolean cancel(boolean b);
boolean isCancelled();
boolean isDone();
V get() throws java.lang.InterruptedException, java.util.concurrent.ExecutionException;
V get(long l, java.util.concurrent.TimeUnit timeUnit) throws java.lang.InterruptedException, java.util.concurrent.ExecutionException, java.util.concurrent.TimeoutException;
}
从方法名我们就知道什么意思了,这里就不详细讲解了。
Future 就是对于具体的Runnable 或者Callable 任务的执行结果进行取消、查
询是否完成、获取结果。必要时可以通过get 方法获取执行结果,该方法会阻塞
直到任务返回结果。
我们在前面看到线程的中止方法并没有cancel这种,它的核心其实也是调用interrupt(),所以能不能成功中止线程还是要看具体的操作者有没有添加对应的判断,愿意停下来不。
本篇算是一个科普,讲的都是基础,很多都是概念,但是还是有必要了解的东西。