java.util.concurrent包中的常见类
1.ReentrantLock锁
a.两个线程并发执行,针对同一个变量各自自增50000次,这个过程是线程不安全的,数据最终的结果也不是100000,如何保证线程安全,加锁,synchronized和ReentrantLock锁
import java.util.concurrent.locks.ReentrantLock;
public class Test01 {
static class TestIncrease{
int i;
ReentrantLock reentrantLock=new ReentrantLock();
public void increase(){
reentrantLock.lock();
i++;
reentrantLock.unlock();
}
}
public static void main(String[] args) {
TestIncrease testIncrease=new TestIncrease();
Thread t1=new Thread(){
@Override
public void run(){
for(int i=0;i<50000;i++){
testIncrease.increase();
}
}
};
t1.start();
Thread t2=new Thread(){
@Override
public void run(){
for(int i=0;i<50000;i++){
testIncrease.increase();
}
}
};
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(testIncrease.i);
}
}
b.为什么引入ReentrantLock锁?
ReentrantLock锁是对synchronized锁的补充
1.这个锁加锁解锁是分开写的,lock()和unlock()方法;
2.提供了一个新的tryLock()方法;
3.这个锁是公平锁;
4.synchronized锁的等待唤醒采用的是wait()、notify()方法,唤醒线程是随机唤醒线程,
而ReentrantLock锁的等待唤醒采用的是Condition类提供的唤醒方法,可以指定唤醒具体的线程。
tryLock()方法和lock()方法的区别:
tryLock()方法:尝试加锁,加锁失败,不会阻塞等待
lock()方法:尝试加锁,加锁失败,会阻塞等待
c.synchronized和ReentrantLock方法适用场景是哪些?
当锁竞争比较小的时候,加锁可以采用synchronized,效率更高,可以自动释放,更便捷。
当锁竞争比较大的时候,加锁可以采用ReentrantLock,可以采用其中的tryLock(),更灵活的控制加锁
如果要使用公平锁,可以采用ReentrantLock锁。
d.已经存在synchronized,为什么juc当中会提出ReentrantLock?
1.ReentrantLock加锁解锁是分开使用的,使用比较灵活,lock(),unlock();
2.线程尝试获取synchronized锁,如果获取不到,就会进入阻塞状态,线程尝试获取ReentrantLock锁,采用的方法是unlock(),如果获取不到,不会进入阻塞状态。
3.公平锁,非公平锁:ReentrantLock可以指定是公平锁还是非公平锁,但是,synchronized只能是非公平锁。
4.等待唤醒机制:synchronized锁等待唤醒机制是利用wait()和notify()来实现的,ReentrantLock是利用Condition类中的等待唤醒机制来实现的,可以指定唤醒具体的线程。而notify()是随机唤醒线程。
2.原子类
原子类是CAS实现的
import java.util.concurrent.atomic.AtomicInteger;
public class Test02 {
static class MyTest{
AtomicInteger atomicInteger=new AtomicInteger(0);
public void increase(){
atomicInteger.getAndIncrement();
}
}
public static void main(String[] args) {
MyTest myTest=new MyTest();
Thread t=new Thread(){
@Override
public void run(){
for (int i=0;i<50000;i++){
myTest.increase();
}
}
};
t.start();
Thread t2=new Thread(){
@Override
public void run(){
for (int i=0;i<50000;i++){
myTest.increase();
}
}
};
t2.start();
try {
t.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(myTest.atomicInteger.get());
}
}
b.原子类一般会使用在监视服务器方面
1.会监视服务器单位时间内收到的请求;
2.监视某个代码块执行了多长时间;
3.监视哪个方法被调用的次数比较多;
4.服务器内部某个类被创建出多少实例。
3.信号量-semaphore
信号量表示可以资源数
举例说明信号量的使用:
借助Java标准库里面的提供的信号量:Semaphore
import java.util.concurrent.Semaphore;
public class Test01 {
public static void main(String[] args) {
//定义可以用的资源数为4
Semaphore semaphore =new Semaphore(4);
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
System.out.println("尝试申请资源");
semaphore.acquire();
System.out.println("已经获取资源");
Thread.sleep(1000);
semaphore.release();
System.out.println("已经释放资源");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i=0;i<20;i++){
Thread t=new Thread(runnable);
t.start();
}
}
}
举例说明信号量的使用场景/对上面代码的解释:
有四个可用资源,p操作是申请资源,V操作是释放资源
总共用20个线程,前四个线程申请资源并且获取到资源,如果前四个线程没有释放资源,就会处于阻塞状态。当前面几个线程释放资源之后,后面的线程可以获取资源。
Semaphore可以实现"共享锁"这样的概念。
4.CountDownLatch
同步等待N个任务执行完毕
countDown():每执行完一个任务,数量减一
await():发生阻塞,直到所有任务执行完毕
代码举例:有八个人一起比赛50m跑步,当所有人都到达终点之后,比赛完毕
import java.util.concurrent.CountDownLatch;
public class Test03 {
public static void main(String[] args) throws InterruptedException {
//计数:8
CountDownLatch countDownLatch=new CountDownLatch(8);
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("起跑");
//用线程等待时间----》模拟每一位运动员的跑步时间
try {
Thread.sleep((long)(Math.random()*8000));
System.out.println("到达终点");
countDownLatch.countDown();//相当于是减减操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i=0;i<8;i++){
Thread t=new Thread(runnable);
t.start();
}
//阻塞等待,直到countDownLatch值减到0为止
countDownLatch.await();
System.out.println("全部到达终点");
}
}
结果展示:
5.ConcurrentHashMap
线程安全的
HashTable也是线程安全的,但是,现在基本不使用
6.HashTable和ConcurrentHashMap的区别是什么?
1.锁冲突概率不一样。HashTable是针对整个Hash表进行加锁,而ConcurrentHashMap是针对每一个hash桶进行加锁(jdk1.8),锁粒度小,所以,ConcurrentHashMap的锁冲突概率小。
2.针对读操作不一样,ConcurrentHashMap中数据是被volatile关键字修饰的,直接操作内存,不需要加锁,而HashTable读数据需要加锁。
3.修改操作不一样,ConcurrentHashMap修改size属性(元素个数),采用的是CAS机制,HashTable是直接加锁。
4.扩容机制不一样,HashTable是直接扩容一个更大的内存,将旧的数据拷贝到新的内存中(一次性全部拷贝),ConcurrentHashMap内存分为两个部分,新数据部分和旧数据部分,(一部分一部分数据拷贝),在这个过程中,当增加元素的时候,往新内存中添加元素,当查找元素的时候,从新内存和旧内存中读取元素。
7.HashMap、HashTable、ConcurrentHashMap的区别是什么?
1.HashMap不是线程安全的,允许key为null;
2.HashTable是线程安全的,针对整个hashTable表进行加锁,key不为null;
3.ConcurrentHashMap是线程安全的,针对每个链表的头节点进行加锁,减少了锁冲突概率。读操作不需要加锁,使用volatile关键字,修改size操作,使用CAS机制,key不为null。
关于ConcurrentHashMap的补充问题
1.ConcurrentHashMap读操作需要加锁吗?
不需要,读操作使用volatile关键字,直接操作内存读取。
2.ConcurrentHashMap锁分段技术是什么?
在jdk1.8版本中,ConcurrentHashMap采用synchronized锁针对每个链表的头节点进行加锁。而在jdk1.8版本之前,把所有的哈希桶分成若干段,针对每段进行加锁。目的都是为了减少锁竞争、锁冲突。
3.ConcurrentHashMap在jdk1.8版本中,做了哪些优化?
a.锁的粒度变小了,从原来的锁分段到现在针对每个哈希桶进行加锁,进一步减少了锁冲突的概率。
b.底层数据结构发生了一点变化,在原来的版本当中,底层数据结构是数组+链表,而现在的数据结构是数组+链表/红黑树(什么时候转化为红黑树?数组长度大于64,链表长度大于8)
8.线程同步的方式
可以借助synchronized、Semaphore、ReentrantLock来实现线程同步