目录
synchronized
同步方法
同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时, 需同步执行。
public synchronized void test(){
System.out.println("测试一下");
}
锁定临界对象
同步代码块在执行时,是锁定 object 对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。
public synchronized void test(){
System.out.println("测试一下");
}
锁定当前对象
public void test(){
synchronized(this){
System.out.println("测试一下");
}
}
锁定当前对象与锁定代码块都等同于:锁定对象,在堆内存中的结构类似于:
锁定类对象
public void test(){
synchronized(A.class){
System.out.println("测试一下");
}
}
锁定静态代码
public synchronized static void test(){
System.out.println("测试一下");
}
锁定静态代码和锁定类对象一致,所以同一个类的所有对象,如果执行同一个方法都会有竞争关系
volatile 关键字
变量的线程可见性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile 修饰的变量是线程可见的,当 JVM 解释 volatile 修饰的变量时,会通知 CPU,在计算过程中, 每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。
volatile 只是通知底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同步。但是和sync不同的是,volatile只保证了可见性,不保证原子性.所以在高并发的情况下,volatile会出现数据不一致的情况.所以不能用volatile代替sync.
wait¬ify
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyContainer4 {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer4 c = new MyContainer4();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2启动");
if(c.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
//通知t1继续执行
lock.notify();
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized(lock) {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
lock.notify();
//释放锁,让t2得以执行
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
AtomicXxx 类型组
原子类型。
在 concurrent.atomic 包中定义了若干原子类型,这些类型中的每个方法都是保证了原子操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如果某数据需要多个线程同时操作,且要求计算原子性,可以考虑使用原子类型对象。
AtomicInteger count = new AtomicInteger(0);
void m(){
count.incrementAndGet();
}
注意:原子类型中的方法是保证了原子操作,但多个方法之间是没有原子性的。如
AtomicInteger i = new AtomicInteger(0);
if(i.get() != 5){
i.incrementAndGet();
}
CountDownLatch 门闩
门闩是 concurrent 包中定义的一个类型,是用于多线程通讯的一个辅助类型。
门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门
闩数量大于 0,线程会阻塞等待。当线程调用 countDown 时,会递减门闩的数量,当门闩数量为 0 时,await 阻塞线程可执行。
CountDownLatch latch = new CountDownLatch(5);
void m1(){
try {
latch.await();// 等待门闩开放。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1() method");
}
void m2(){
for(int i = 0; i < 10; i++){
if(latch.getCount() != 0){
System.out.println("latch count : " + latch.getCount());
latch.countDown(); // 减门闩上的锁。
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("m2() method : " + i);
}
锁的重入
在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。
当线程持有锁时,会在 monitor 的计数器中执行递增计算,若当前线程调用其他同步代码,且同步代码的锁对象相同时,monitor 中的计数器继续递增。每个同步代码执行结束,
monitor 中的计数器都会递减,直至所有同步代码执行结束,monitor 中的计数器为 0 时,释放锁标记,_Owner 标记赋值为 null。
ReentrantLock
重入锁,建议应用的同步方式。相对效率比 synchronized 高。量级较轻。
synchronized 在 JDK1.5 版本开始,尝试优化。到 JDK1.7 版本后,优化效率已经非常好了。在绝对效率上,不比 reentrantLock 差多少。
使用重入锁,必须必须必须手工释放锁标记。一般都是在 finally 代码块中定义释放锁标记的 unlock 方法。
ReentrantLock有公平锁和不公平锁,如果公平锁的意思是如果一个线程等待时间最长,当线程释放之后,把锁给等待时间最长的线程;不公平锁指所有的线程不管是新进来的还是后进来的是否能抢到锁的概率是一样的.
private static ReentrantLock lock = new ReentrantLock(true);
public void run(){
for(int i = 0; i < 5; i++){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " get lock");
}finally{
lock.unlock();
}
}
}
同步容器
解决并发情况下的容器线程安全问题的。给多线程环境准备一个线程安全的容器对象。线程安全的容器对象: Vector, Hashtable。线程安全容器对象,都是使用 synchronized方法实现的。
concurrent 包中的同步容器,大多数是使用系统底层技术实现的线程安全。类似 native。
Java8 中使用 CAS。
Map/Set
ConcurrentHashMap/ConcurrentHashSet
底层哈希实现的同步 Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。量级较 synchronized 低。key 和 value 不能为 null。在并发性比较高的情况下,用ConcurrentHashMap ,如果并发性高且要排序的情况下,用ConcurrentSkipListMap。
ConcurrentSkipListMap/ConcurrentSkipListSet
底层跳表(SkipList)实现的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低,因为ConcurrentSkipListMap和ConcurrentSkipListSet插入时效率比较低,需要排好顺序。但是查的时候效率很高
List
CopyOnWriteArrayList
CopyOnWriteArrayList在多线程环境下,写时效率低,读时效率高,适合写少读多的环境,比如事件监听器。
Queue
ConcurrentLinkedQueue并发队列(内部加锁的)
LinkedBlockingQueue
阻塞队列,队列容量不足自动阻塞,队列容量为 0 自动阻塞。
ArrayBlockingQueue
底层数组实现的有界队列。自动阻塞。根据调用 API(add/put/offer)不同,有不同特性。
当容量不足的时候,有阻塞能力。
add 方法在容量不足的时候,抛出异常。
put 方法在容量不足的时候,阻塞等待。
offer 方法,
单参数 offer 方法,不阻塞。容量不足的时候,返回 false。当前新增数据操作放弃。三参数 offer 方法(offer(value,times,timeunit)),容量不足的时候,阻塞 times 时长(单
位为 timeunit),如果在阻塞时长内,有容量空闲,新增数据返回 true。如果阻塞时长范围
内,无容量空闲,放弃新增数据,返回 false。
DelayQueue
延时队列。根据比较机制,实现自定义处理顺序的队列。常用于定时任务。如:定时关机。
LinkedTransferQueue
转移队列,使用 transfer 方法,实现数据的即时处理。没有消费者,就阻塞。适用场景:消费者先启动,生产者生产一个东西的时候,不扔在队列里,而是直接去找有没有消费者,有的话直接扔给消费者,若没有消费者线程,调用transfer()方法就会阻塞,调用add()、offer()、put()方法不会阻塞。TransferQueue适用于更高的并发情况
SynchronusQueue(特殊的TransferQueue,容量为0)
扔在队列的东西必须被消费者马上消费掉,否则就会出问题。
同步队列,是一个容量为 0 的队列。是一个特殊的 TransferQueue。必须现有消费线程等待,才能使用的队列。
add 方法,无阻塞。若没有消费线程阻塞等待数据,则抛出异常。
put 方法,有阻塞。若没有消费线程阻塞等待数据,则阻塞。
/*一种特殊的TransferQueue,生产的任何一个东西必须直接交给消费者消费,不能搁在容器里,容器的容量为0*/
public class SynchronizeQueueTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> strs = new SynchronousQueue<>();
new Thread(()->{ //消费者线程
try {
System.out.println(strs.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
strs.put("aaaa"); //不能调用add(报错),add不进去,put阻塞,等待消费者消费,内部调用的transfer.
System.out.println(strs.size()); //0
}
ThreadPool&Executor
Executor
线程池顶级接口。
常用方法 - void execute(Runnable)
作用是: 启动线程任务的。
ExecutorService
Executor 接口的子接口。
常见方法 - Future submit(Callable), Future submit(Runnable)
Future(是有返回值的)
未来结果,代表线程任务执行结束后的结果。
Callable
可执行接口。
接口方法 : Object call();相当于 Runnable 接口中的 run 方法。区别为此方法有返回值。不能抛出已检查异常。
和 Runnable 接口的选择 - 需要返回值或需要抛出异常时,使用 Callable,其他情况可任意选择。
Executors
工具类型。为 Executor 线程池提供工具方法。类似 Arrays,Collections 等工具类型的功用。
FixedThreadPool
容量固定的线程池
queued tasks - 任务队列
completed tasks - 结束任务队列
CachedThreadPool
缓存的线程池。容量不限(Integer.MAX_VALUE)。自动扩容。默认线程空闲 60 秒,自动销毁。
ScheduledThreadPool
计划任务线程池。可以根据计划自动执行任务的线程池。
SingleThreadExceutor
单一容量的线程池。
ForkJoinPool
分支合并线程池(mapduce 类似的设计思想)。适合用于处理复杂任务。初始化线程容量与 CPU 核心数相关。
线程池中运行的内容必须是 ForkJoinTask 的子类型(RecursiveTask,RecursiveAction)。
WorkStealingPool
JDK1.8 新增的线程池。工作窃取线程池。当线程池中有空闲连接时,自动到等待队列中窃取未完成任务,自动执行。
初始化线程容量与 CPU 核心数相关。此线程池中维护的是精灵线程。
ExecutorService.newWorkStealingPool();
ThreadPoolExecutor
线程池底层实现。除 ForkJoinPool 外,其他常用线程池底层都是使用 ThreadPoolExecutor
实现的。
public ThreadPoolExecutor
(int corePoolSize, // 核心容量
int maximumPoolSize, // 最大容量
long keepAliveTime, // 生命周期,0 为永久
TimeUnit unit, // 生命周期单位
BlockingQueue workQueue // 任务队列,阻塞队列。
);