JAVA 线程实现/创建方式
1、继承Thread
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
2、实现Runnable接口
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
3、实现callable接口
实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。执行 Callable 方式,
需要 FutureTask 实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类
class ThreadTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 666;
}
}
class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest threadTest = new ThreadTest();
FutureTask<Integer> result = new FutureTask<>(threadTest);
new Thread(result).start();
System.out.println(result.get());
}
}
synchronized关键字
1、修饰代码块
锁的是当前对象
public void run(){
synchronized(this){
//todo
}
}
//只有一个对象,实现同步
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
//多个对象,不是同一把锁,两个线程互不干扰
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
这时创建了两个SyncThread的对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
确定需要锁的对象写法
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
不确定需要锁的对象写法
class Test implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
public void method()
{
synchronized(lock) {
// todo 同步代码块
}
}
public void run() {
}
}
2、修饰非静态方法
锁定了整个方法时的内容
public synchronized void method()
{
// todo
}
3、修饰静态方法
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象
public synchronized static void method() {
// todo
}
4、修饰类
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
synchronized教程地址:https://www.cnblogs.com/pingchuanxin/p/8473288.html
synchronized跟Lock锁区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
公平锁与非公平锁的区别
1、公平锁 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程
2、非公平锁 指在分配锁时不考虑线程排队等待的情况,随机分配锁
3、公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多
什么是可重入锁?
可重入锁指的是在一个线程中可以多次获取同一把锁
比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
在java中ReentrantLock和synchronized都是可重入锁
Lock锁
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
unLock()方法是用来释放锁的
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock(long time, TimeUnit unit)和tryLock()尝试获取锁
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly中断等待状态的线程
当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
中断的线程会抛出InterruptedException异常
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
即Lock接口的方法由ReentrantLock调用
private Lock lock = new ReentrantLock();
lock.lock();//上锁
ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
readLock可以多个线程同时执行
writeLock只有一个线程可以执行,其他线程需要等到释放写锁才能执行
ReentrantLock教程地址:https://www.cnblogs.com/bsjl/p/7654618.html
生产者消费者问题
1. wait() / notify()方法
当缓冲区已满时,生产者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行;
当缓冲区已空时,消费者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行。
当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;
当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
/**
* 仓库
*/
public class Storage {
private final int Max = 10;
private LinkedList<Object> list = new LinkedList<>();
// 生产
public void produce() {
synchronized (list) {
while (list.size() + 1 > Max) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
list.notifyAll();
}
}
// 消费
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
list.notifyAll();
}
}
}
/**
* 生产者
*/
public class Producer implements Runnable {
private Storage storage;
public Producer() {
}
public Producer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者
*/
public class Consumer implements Runnable {
private Storage storage;
public Consumer() {
}
public Consumer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 测试类
*/
public class test {
public static void main(String[] args) {
Storage storage = new Storage();
Thread p1 = new Thread(new Producer(storage));
Thread p2 = new Thread(new Producer(storage));
Thread p3 = new Thread(new Producer(storage));
Thread c1 = new Thread(new Consumer(storage));
Thread c2 = new Thread(new Consumer(storage));
Thread c3 = new Thread(new Consumer(storage));
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
2. await() / signal()方法
在JDK5中,用ReentrantLock和Condition可以实现等待/通知模型,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。
- await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
- await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
- awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
- awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
- signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
- signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
在这里只需改动Storage类
package test1;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Storage {
// 仓库最大存储量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的条件变量
private final Condition full = lock.newCondition();
// 仓库空的条件变量
private final Condition empty = lock.newCondition();
public void produce()
{
// 获得锁
lock.lock();
while (list.size() + 1 > MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
empty.signalAll();
lock.unlock();
}
public void consume()
{
// 获得锁
lock.lock();
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
full.signalAll();
lock.unlock();
}
}
3. BlockingQueue阻塞队列方法
BlockingQueue是JDK5.0的新增内容,它是一个已经在内部实现了同步的队列,实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
import java.util.concurrent.LinkedBlockingQueue;
public class Storage {
// 仓库存储的载体
private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<>(10);
public void produce() {
try{
list.put(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
} catch (InterruptedException e){
e.printStackTrace();
}
}
public void consume() {
try{
list.take();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费了一个产品,现库存" + list.size());
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
可能会出现put()或take()和System.out.println()输出不匹配的情况,是由于它们之间没有同步造成的。BlockingQueue可以放心使用,这可不是它的问题,只是在它和别的对象之间的同步有问题。
【生产者Thread-0】生产一个产品,现库存3
【生产者Thread-2】生产一个产品,现库存3
【生产者Thread-1】生产一个产品,现库存3
【生产者Thread-0】生产一个产品,现库存4
【生产者Thread-1】生产一个产品,现库存6
【生产者Thread-2】生产一个产品,现库存5
【消费者Thread-3】消费了一个产品,现库存4
【消费者Thread-5】消费了一个产品,现库存4
【消费者Thread-4】消费了一个产品,现库存3
ArrayBlockingQueue与LinkedBlockingQueue
区别:https://www.jianshu.com/p/5b85c1794351
使用场景:https://blog.csdn.net/jiangguilong2000/article/details/11617529
4. Semaphore信号量
Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。计数为0的Semaphore是可以release的,然后就可以acquire(即一开始使线程阻塞从而完成其他执行。)
主要方法:
- void acquire() :从信号量获取一个许可,如果无可用许可前将一直阻塞等待,
- void acquire(int permits) :获取指定数目的许可,如果无可用许可前也将会一直阻塞等待
- boolean tryAcquire():从信号量尝试获取一个许可,如果无可用许可,直接返回false,不会阻塞
- boolean tryAcquire(int permits): 尝试获取指定数目的许可,如果无可用许可直接返回false
- boolean tryAcquire(int permits, long timeout, TimeUnit unit): 在指定的时间内尝试从信号量中获取许可,如果在指定的时间内获取成功,返回true,否则返回false
- void release(): 释放一个许可,别忘了在finally中使用,注意:多次调用该方法,会使信号量的许可数增加,达到动态扩展的效果,如:初始permits为1, 调用了两次release,最大许可会改变为2
- int availablePermits(): 获取当前信号量可用的许可
public class Storage {
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 仓库的最大容量
final Semaphore notFull = new Semaphore(10);
// 将线程挂起,等待其他来触发
final Semaphore notEmpty = new Semaphore(0);
// 互斥锁,只有一个线程可以得到许可
final Semaphore mutex = new Semaphore(1);
public void produce() {
try {
// 获取许可
notFull.acquire();
mutex.acquire();
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放
mutex.release();
notEmpty.release();//初始0多次释放后数值会增加
}
}
public void consume() {
try {
// 获取许可
notEmpty.acquire();
mutex.acquire();
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放
mutex.release();
notFull.release();
}
}
}
5. 管道
具体实现查看教程
生产者消费者问题教程地址:https://blog.csdn.net/ldx19980108/article/details/81707751
Java中的锁分类
- 公平锁/非公平锁
- 可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。
详细介绍:https://www.cnblogs.com/qifengshi/p/6831055.html
CopyOnWriteArrayList
一、CopyOnWriteArrayList介绍
①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
③、CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
二、CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
(1、young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
2、年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC
)
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
三、CopyOnWriteArrayList的一些方法
1、add(E e) :将指定元素添加到此列表的尾部,返回值为boolean。
2、add(int index, E element) : 在此列表的指定位置上插入指定元素。
3、clear():从此列表移除所有元素。
4、contains(Object o) :如果此列表包含指定的元素,则返回 true。
5、equals(Object o) :比较指定对象与此列表的相等性。
6、get(int index) : 返回列表中指定位置的元素。
7、hashCode() : 返回此列表的哈希码值。
8、indexOf(E e, int index) : 返回第一次出现的指定元素在此列表中的索引,从 index 开始向前搜索,如果没有找到该元素,则返回 -1。
9、indexOf(Object o) :返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
10、isEmpty() :如果此列表不包含任何元素,则返回 true。
11、iterator() :返回以恰当顺序在此列表元素上进行迭代的迭代器,返回值为 Iterator。
12、lastIndexOf(E e, int index) :返回最后一次出现的指定元素在此列表中的索引,从 index 开始向后搜索,如果没有找到该元素,则返回 -1。
13、lastIndexOf(Object o) : 返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。
14、remove(int index) :移除此列表指定位置上的元素。
15、remove(Object o) :从此列表移除第一次出现的指定元素(如果存在),返回值为 boolean。
16、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。
17、size() :返回此列表中的元素数。
18、subList(int fromIndex, int toIndex) :返回此列表中 fromIndex(包括)和 toIndex(不包括)之间部分的视图,返回值为 List 。
总结
CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
使用讲解:https://www.cnblogs.com/fsmly/p/11298782.html
原理优缺点:https://www.cnblogs.com/yangfei629/p/11530968.html
CopyOnWriteArrayset
一、CopyOnWriteArraySet介绍
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet,有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类都继承于共同的父类;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。和CopyOnWriteArrayList类似,CopyOnWriteArraySet具有以下特性:
1、它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
2、它是线程安全的。
3、因为通常需要复制整个基础数组,所以可变操作(add()、set() 和remove() 等等)的开销很大。
4、迭代器支持hasNext(), next()等不可变操作,但不支持可变remove()等操作。
5、使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
二、CopyOnWriteArraySet的一些方法
1、add(E e) :如果指定元素并不存在于此 set 中,则添加它,返回值为Boolean。
2、clear():移除此 set 中的所有元素。
3、contains(Object o) : 如果此 set 包含指定元素,则返回 true。
4、equals(Object o) :比较指定对象与此 set 的相等性,返回值为 boolean 。
5、isEmpty() :如果此 set 不包含任何元素,则返回 true。
6、iterator() :返回按照元素添加顺序在此 set 中包含的元素上进行迭代的迭代器,返回值为 Iterator。
7、remove(Object o) :如果指定元素存在于此 set 中,则将其移除,返回值为 boolean 。
8、size() :返回此 set 中的元素数目。
9、Object[] toArray() : 返回一个包含此 set 所有元素的数组。
使用教程:https://www.cnblogs.com/xiaolovewei/p/9142046.html
使用教程2:https://blog.csdn.net/weixin_42146366/article/details/88019292
ConcurrentHashMap
HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容重哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸多不便,虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 —— ConcurrentHashMap。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
使用教程:https://blog.csdn.net/cx897459376/article/details/106427587/
CountDownLatch
1、类介绍
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
示例
public class CountDownLatchTest {
private static final int RUNNER_COUNT = 10;
public static void main(String[] args) throws InterruptedException {
final CountDownLatch begin = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(RUNNER_COUNT);
//创建线程池
final ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < RUNNER_COUNT; i++) {
final int NO = i + 1;
Runnable run = new Runnable() {
@Override
public void run() {
try {
// 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
begin.await();
Thread.sleep((long) (Math.random() * 10000));
System.out.println("No." + NO + " arrived");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 计数-1
end.countDown();
}
}
};
exec.submit(run);
}
System.out.println("Game Start ...");
// 计数-1
begin.countDown();
// 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
end.await();
// 线程阻塞一段时间,如果计数依然不是0,则返回false;否则返回true
// end.await(30, TimeUnit.SECONDS);
System.out.println("Game Over.");
// 启动有序关闭
exec.shutdown();
}
}
CyclicBarrier
栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。 理解:设定一个值(线程的数量),每个线程启动后进入await方法,当所有线程都运行后,去到锁的线程才会继续执行,否则处于堵塞状态
教程地址:https://blog.csdn.net/qq_38293564/article/details/80558157
SynchronousQueue
SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。
教程地址:https://www.cnblogs.com/hongdada/p/6147834.html
Java自定义线程池和七个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//todo}
一、corePoolSize 线程池核心线程大小 二、maximumPoolSize 线程池最大线程数量 三、keepAliveTime 空闲线程存活时间 四、unit 空闲线程存活时间单位 五、workQueue 工作队列 六、threadFactory 线程工厂 七、handler 拒绝策略 教程地址:https://blog.csdn.net/weixin_38938840/article/details/104774426
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。 函数式接口可以被隐式转换为 lambda 表达式。 如定义了一个函数式接口如下:
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
greetService1.sayMessage("666")
消费型接口
接口唯一的抽象方法是:
public interface Consumer<T> {
void accept(T T);
}
这是一个单参数,无返回值的方法,参数是泛型类。这个接口被称为消费型接口,因为没有返回值,接口里面干了什么和调用方没什么关系。
这种单参数无返回值的接口我们可以这么用Lambda表达式:
Consumer consumer = (a) -> System.out.println("this is " + a);
consumer.accept("123");
供给型接口
接口唯一的抽象方法是:
public interface Consumer<T> {
T accept();
}
这是一个无参数,有返回值的方法,返回值类型是泛型类。这个接口被称作供给型接口。
这种无参数有返回值的方法我们可以这么用:
Consumer<String> consumer=()-> "666";
String str = consumer.accept();
System.out.println(str);
Steam流式算法
Stream 流处理,首先要澄清的是 java8 中的 Stream 与 I/O 流 InputStream 和 OutputStream 是完全不同的概念。
Stream 机制是针对集合迭代器的增强。流允许你用声明式的方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)
教程地址:https://www.cnblogs.com/gaopengfirst/p/10813803.html
扩展------Java双冒号(::)运算符的使用
以下是Java 8中方法引用的一些语法:
静态方法引用(static method)语法:classname::methodname 例如:Person::getAge
对象的实例方法引用语法:instancename::methodname 例如:System.out::println
对象的超类方法引用语法: super::methodname
类构造器引用语法: classname::new 例如:ArrayList::new
数组构造器引用语法: typename[]::new 例如: String[]:new
如果上的语法太枯燥,那就通过一些例子来加强对它的理解:
教程地址:https://blog.csdn.net/zhoufanyang_china/article/details/87798829
ForkJoin
fork/join框架相当于一个map/reduce的过程,先将一个大的任务分解成几个小模块,再将几个小模块继续分解成子模块,直到达到可以处理的阈值,最后再将各个子模块的结果进行汇总。 ForkJoinPool: 线程池最大的特点就是分叉(fork)合并(join)模式,将一个大任务拆分成多个小任务,并行执行,再结合工作窃取算法提高整体的执行效率,充分利用CPU资源。
//异步执行给定任务的排列,无返回值
ForkJoinPool.execute()
//执行给定的任务,在完成后返回其结果
ForkJoinPool.invoke()
//提交一个ForkJoinTask来执行,有返回值
ForkJoinPool.submit()
ForkJoinTask: 运行在ForkJoinPool的一个任务抽象,可以理解为类线程但是比线程轻量的实体,在ForkJoinPool中运行的少量ForkJoinWorkerThread可以持有大量的ForkJoinTask和它的子任务,同时也是一个轻量的Future,使用时应避免较长阻塞或IO。
继承子类:
RecursiveAction:递归无返回值的ForkJoinTask子类;
RecursiveTask<T>:递归有返回值的ForkJoinTask子类;核心方法:
fork():在当前线程运行的线程池中创建一个子任务;
join():模块子任务完成的时候返回任务结果;
invoke():执行任务,也可以实时等待最终执行结果;
使用流程:
1、任务类继承RecursiveTask<T>或者RecursiveAction,这里只是区分要不要返回值
2、使用ForkJoinPool执行任务
累加示例
package test3;
import java.util.concurrent.RecursiveTask;
/**
* 累加任务
*/
public class ForkJoinTest extends RecursiveTask<Long> {
private Long star;
private Long end;
private Long temp;
public ForkJoinTest(Long star, Long end, Long temp) {
this.star = star;
this.end = end;
//临界值
this.temp = temp;
}
@Override
protected Long compute() {
//没有超出临界值
if ((end - star) < temp) {
Long sum = 0L;
for (Long i = star; i <= end; i++) {
sum += i;
}
return sum;
} else {
//中间值
Long middle = (star + end) / 2;
//本质还是递归
ForkJoinTest FJ1 = new ForkJoinTest(star, middle, temp);
FJ1.fork();
ForkJoinTest FJ2 = new ForkJoinTest(middle + 1, end, temp);
FJ2.fork();
return FJ1.join() + FJ2.join();
}
}
}
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 10_0000_0000L, 10L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
Long l = submit.get();
System.out.println(l);
}
}
异步回调
runAsync方法不支持返回值。
示例
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步调用无返回值
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
//正常打印
System.out.println("进入异步任务");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用completableFuture.get()才会打印
System.out.println("延时打印");
});
System.out.println("主线程任务");
//获取阻塞执行结果
completableFuture.get();
}
}
supplyAsync可以支持返回值。
public class test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("进入了异步任务");
// int i = 10 / 0;
return 1024;
});
// 成功的回调
completableFuture.whenComplete((t, u) -> {
// t 正常的返回结果
System.out.println("t=" + t);
// u 错误的返回信息
System.out.println("u=" + u);
}).exceptionally((e) -> { // 失败回调
System.out.println(e.getMessage());
return 404;
}).get();
}
}
Java内存模型- JMM(Java Memory Model)
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量是主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
下面的示例将变量i加上volatile修饰后,新线程会跳出循环
不可见性问题:
示例:运行后新建的线程会一直运行,如果循环内有其他操作,线程会跳出循环,因为i的值已经被修改?
public class test3 {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (i == 0) {
}
}).start();
TimeUnit.SECONDS.sleep(1);
i = 1;
System.out.println("main线程i的值:" + i);
}
}
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
教程地址:https://zhuanlan.zhihu.com/p/29881777
volatile
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
特性
1.保证可见性,不保证原子性
2.禁止指令重排
教程地址:https://blog.csdn.net/u012723673/article/details/80682208
CAS的概念
CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制。 CAS(V, A, B),V为内存地址、A为预期原值,B为新值。如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值。否则,说明已经被其他线程更新,处理器不做任何操作;无论哪种情况,它都会在 CAS 指令之前返回该位置的值。而我们可以使用自旋锁,循环CAS,重新读取该变量再尝试再次修改该变量,也可以放弃操作。
为什么需要CAS机制呢?我们先从一个错误现象谈起。我们经常使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:
(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中
在单线程状态下这个操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象。
Volatile关键字可以保证线程间对于共享变量的可见性可有序性,可以防止CPU的指令重排序(DCL单例),但是无法保证操作的原子性,所以jdk1.5之后引入CAS利用CPU原语保证线程操作的院子性。
CAS操作由处理器提供支持,是一种原语。原语是操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。如 Intel 处理器,比较并交换通过指令的 cmpxchg 系列实现。
AtomicReference
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。
顺便说一下:引用类型的赋值是原子的。虽然虚拟机规范中说64位操作可以不是原子性的,可以分为两个32位的原子操作,但是目前商用的虚拟机几乎都实现了64位原子操作。
public class Test6 {
public static void main(String[] args) throws InterruptedException {
User user1 = new User("ykk", 18);
User user2 = new User("czz", 19);
User user3 = new User("yxx", 2);
AtomicReference<User> AR = new AtomicReference<>();
AR.set(user1);
System.out.println("初始值:" + AR.get().toString());
AR.compareAndSet(user1, user2);
System.out.println("如果当前值==为预期值,则将值设置为给定的更新值。" + AR.get().toString());
User u = AR.getAndSet(user3);
System.out.println("将原子设置为给定值并返回旧值。" + u.toString());
System.out.println("新的值:" + AR.get().toString());
}
static class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}