对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
一个进程至少包含一个主线程,也可以有更多的子线程。
线程拥有自己的栈空间。
同一时间,CPU只能处理1条线程,只有一条线程在工作(执行)
多线程并发(同时)执行,其实质是CPU快速的在多线程之间调度(切换)
不是JVM,而是JVM通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。
CPU在N多条线程中调度,会消耗大量的cpu资源
每条线程被调度执行的频率越低(线程的执行效率低)
能适当提高程序的执行效率
能适当提高资源的利用率(CPU 内存利用率等)
创建线程是有开销的
如果开启大量的线程,会降低程序的性能
程序设计更加复杂:线程之间的通讯,多线程的数据共享
显示和刷新UI界面
处理UI事件(比如点击事件,滚动事件,拖拽事件等)
别将比较耗时的操作放在主线程中,会导致UI界面的卡顿
将耗时操作放在子线程(后台线程,非主线程)
释放锁的操作
不会释放锁的操作
在A线程中调用B线程的 join() 方法,表示:
当执行到此方法, A 线程停止执行,直至 B线程 执行完毕,A线程再接着 join() 之后的代码执行 ,join方法 的原理就是调用相应线程的 wait方法 进行等待操作的。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | - |
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法,使一个线程睡眠 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法,挂起一个线程 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | – |
LockSupport.parkUntil() 方法 | – |
阻塞 和 等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。
而 等待 是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
wait
和notify
方法定义在Object
类中,因此会被所有的类所继承。 这些方法都是final
的,即它们都是不能被重写。 而sleep
方法是在Thread
类中是由native
修饰的,本地方法。wait
会释放锁,而sleep
一直持有锁。wait
方法会释放锁,所以调用该方法时,当前的线程必须拥有当前对象的monitor
,也即锁。要确保调用wait()
方法的时候拥有锁,即wait()
方法的调用必须放在synchronized
方法或synchronized
块中。Wait
通常被用于线程间交互,sleep
通常被用于暂停执行。线程调用start()
方法时是由线程调度运行run()
方法,直接调用run()
方法,是不启动一个线程
java采用共享内存的并发模型
线程间通信使用:
Object
中的wait
、notify
和notifyAll
方法。ReenterantLock
得到的Condition
中的await
、signal
和signalAll
方法。java memory model 与JVM的关系
Java内存模型(Java Memory Model,JMM)JMM
主要是为了规定线程
和内存
之间的一些关系。
根据JMM的设计,系统存在一个主内存
(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存
(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
Java的并发采用的是共享内存模型,Java线程之间的通信总是显性进行。
主内存可以看成是堆,线程A在自己的本地内存中更改了共享变量的值,需要刷新到主内存中,才能同步到线程B。
A线程什么时候刷新主内存,B什么时候同步主内存都是不确定的。
jdk1.5 后 java.util.concurrent.atomic 类的小工具包提供原子变量类:
1.类中的变量都是volatile
类型:保证内存可见性
2.使用CAS
算法:保证数据的原子性(硬件级别的原子操作来实现CAS)
Java 提供了一种稍弱的同步机制,即 volatile
变量,用来确保将变量的更新操作通知到其他线程,可以保证内存中的数据可见。
可以将 volatile
看做一个轻量级的锁,但是又与锁有些不同:
对声明了volatile关键字的变量执行写操作的时候,JVM会向处理器发送一条Lock 前缀的指令,会把这个变量所在缓存行的数据写回到系统内存;
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
参考
使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作。
如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//读操作,没有synchronized,提高性能
public int getValue() {
return value;
}
//写操作,必须synchronized。因为x++不是原子操作
public synchronized int increment() {
return value++;
}
volatile 的一个语义是禁止指令重排序优化(即必须初始化好堆内存后,才将地址赋值给 instance 字段),也就保证了instance 变量被赋值的时候对象已经是初始化过的,从而避免了在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance,判断 instance!=null,返回,但是取到的其实是没有初始化的对象(实例变量还是默认值,而不是初始值),程序就会出错。
pubic class Singleton {
private volatile static Singleton instace;
public static Singleton getInstance(){
//第一次null检查
if(instance == null){
synchronized(Singleton.class) {
//第二次null检查
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
1.类中的变量都是volatile类型:保证内存可见性
2.使用CAS算法:保证数据的原子性
在 Java 中,关键字 synchronized
可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized
另外一个重要的作用,synchronized
可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile
功能),这点确实也是很重要的。
synchronized关键字最主要有以下3种应用方式,下面分别介绍:
同步(Synchronization)基于进入和退出管程(Monitor
)对象实现
monitor
对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized
锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait
等方法存在于顶级对象Object
中的原因。
monitor
是由 ObjectMonitr 实现的,有两个队列,_WaitSet
和 _EntryList
,当多个线程同时访问一段同步代码时,首先会进入 _EntryList
集合,当线程获取到对象的 monitor 后进入 _Owner
区域,若线程调用 锁对象的wait()
方法,将释放当前持有的monitor
,owner
变量恢复为null,进入 WaitSet
集合中等待被唤醒。其主要数据结构如下:
从字节码中可知同步代码块的实现使用的是monitorenter
和 monitorexit
指令。
同步方法的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED
访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
notify/notifyAll
和 wait
方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出 IllegalMonitorStateException
异常,这是因为调用这几个方法前必须拿到当前对象的监视器 monitor
对象,也就是说notify/notifyAll
和wait
方法依赖于monitor
对象,在前面的分析中,我们知道monitor
存在于对象头的Mark Word
中(存储monitor引用指针),而synchronized
关键字可以获取 monitor
,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
锁类型
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在 jvm 层面上 | 是一个接口类,ReentrantLock 实现了 Lock 接口 |
锁的释放 | 1、获取锁的线程执行完同步代码,自动释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在 finally 中必须显示的释放锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有tryLock() 方法,可以尝试获得锁,如果锁被占用,返回false,否则返回true,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
synchronized 是 JVM 提供的加锁,悲观锁;
lock是Java语言实现的,而且是乐观锁。
ReentrantLock是基于AQS实现的,由于AQS是基于FIFO队列的实现
可重入公平锁获取流程
在获取锁的时候,如果当前线程之前已经获取到了锁,就会把state加1,在释放锁的时候会先减1,这样就保证了同一个锁可以被同一个线程获取多次,而不会出现死锁的情况。这就是ReentrantLock的可重入性。
Lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。
ReadWriteLock接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
参考
AbstractQueuedSynchronizer 是java并发包中的一个抽象类,该类更像是一个框架,提供了一些模板方法供子类实现,从而实现了不同的同步器。
AQS内部维护了一个双向链表,head,tail分别指向头尾。
Node节点封装了尝试获取锁的线程对象。
volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。当state的值为0的时候,标识改Lock不被任何线程所占有。
AQS的很多操作都是基于CAS原子操作的,以确保线程安全。
通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
ThreadPoolExecutor 线程池的实现类
常见构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
线程池的线程执行规则跟任务队列有很大的关系:
ThreadPoolExecutor类中提供了几种饱和策略的写法:
new ThreadPoolExecutor.AbortPolicy()
线程池不使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
线程池模式:先启动一定数目的工作线程。当没有请求工作的时候,所有的工人线程都会等待新的请求过来,一旦有工作到达,就马上从线程池中唤醒某个线程来执行任务,执行完毕后继续在线程池中等待任务池的工作请求的到达。
Client参与者,发送Request的参与者
Channel参与者,负责缓存Request的请求,初始化启动线程,分配工作线程。维护线程池和任务池
Worker参与者,具体执行Request的工作线程
工具类:Executors
方法有:
ExecutorService newFixedThreadPool(): 创建固定大小的线程池
ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需要自动的更改数量。
ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerTest {
public static void main(String args[]) {
final Queue<Integer> sharedQueue = new LinkedList();
Thread producer = new Producer(sharedQueue);
Thread consumer = new Consumer(sharedQueue);
producer.start();
consumer.start();
}
}
class Producer extends Thread {
private static final int MAX_QUEUE_SIZE = 5;
private final Queue sharedQueue;
public Producer(Queue sharedQueue) {
super();
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (sharedQueue) {
while (sharedQueue.size() >= MAX_QUEUE_SIZE) {
System.out.println("队列满了,等待消费");
try {
sharedQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sharedQueue.add(i);
System.out.println("进行生产 : " + i);
sharedQueue.notify();
}
}
}
}
class Consumer extends Thread {
private final Queue sharedQueue;
public Consumer(Queue sharedQueue) {
super();
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
while (true) {
synchronized (sharedQueue) {
while (sharedQueue.size() == 0) {
try {
System.out.println("队列空了,等待生产");
sharedQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int number = (int) sharedQueue.poll();
System.out.println("进行消费 : " + number);
sharedQueue.notify();
}
}
}
}
英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。Java的原生语法中并没有实现协程,Lua语言、Python、Go有协程。
https://blog.csdn.net/zxm1306192988/article/details/81199567
例子
ThreadLocal,线程本地变量,一般声明为static ,ThreadLocal为变量在每个线程中都创建了一个副本,不同线程只能从中 get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal.get(): 获取ThreadLocal中当前线程中保存的变量副本。
2、ThreadLocal.set(): 设置ThreadLocal中当前线程中变量副本。
3、ThreadLocal.remove(): 移除ThreadLocal中当前线程变量的副本。
4、ThreadLocal.initialValue(): protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,在线程第1次调用get()或set(Object)时才 执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
线程共享变量缓存如下:
Thread有个ThreadLocal.ThreadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。
这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。(ThreadLocalMap是ThreadLocal内部类)
ThreadLocalMap
1、Thread: 当前线程,可以通过Thread.currentThread()获取。
2、ThreadLocal:我们的static ThreadLocal变量。
3、Object: 当前线程共享变量。
我们调用ThreadLocal.get()方法时,实际上是从当前线程中获取ThreadLocalMap
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。
这种存储结构的好处:
1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
2、ThreadLocalMap
关于ThreadLocalMap
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap
虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
缺点:由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
启动一个线程到达就绪状态,不会立刻执行,需要等待CPU的调度,所以多个线程执行顺序不确定。
通过 join 方法去保证多线程的顺序性执行
在A线程中调用B线程的join()方法,表示:当执行到此方法,A线程停止执行,直至B线程执行完毕,A线程再接着join()之后的代码执行 ,join方法的原理就是调用相应线程的wait方法进行等待操作的。
使用 ExecutorService executorService=Executors.newSingleThreadExecutor();
创建只包含一个线程的线程池,他会维护一个FIFO队列,所有submit的任务按顺序执行。
java中的线程分为两种:守护线程(Daemon)和用户线程(User)。
通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。