希望各位大佬能点点赞,关注收藏一下哈,谢谢啦!!
目录
1.ReentrantLock(可重入锁的使用)
ReentrantLock是可重入锁,有的兄弟们可以会问,明明我们已经学了一种可重入锁synchronized了,我们为什么还要学习ReentrantLock呢?这是因为虽然它是Java的远古产物,但是ReentrantLock的功能更加丰富。
我们先来了解一下它的基本使用方法,毕竟他是个锁,最重要的就是加锁咋加。
这里和synchronized不一样,ReentrantLock是一个类,所以我们需要调用它的内部方法lock(或是tryLock区别在下面会讲)和unlock(这个很容易忘记)。
接下来是使用ReentrantLock的方法
public class demo1 {//可重入锁ReentrantLock的使用
static int count;
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();//新建一个ReentrantLock对象
Thread t = new Thread(() -> {
try {
reentrantLock.lock();
for (int i = 0; i < 50000; i++) {
count++;//还是老例子,我们用count加加来测试能不能锁上
}
} finally {
reentrantLock.unlock();
}
});Thread t1 = new Thread(() -> {
try {
reentrantLock.lock();
for (int i = 0; i < 50000; i++) {
count++;
}
} finally {
reentrantLock.unlock();
}
});
t.start();
t1.start();
t1.join();
t.join();
System.out.println(count);
}
}
我们可以看到,结果输出就是100000,说明这锁是可以使用的。那tryLock是什么呢,事实上,它是遇到锁拿锁,没拿到锁就放弃这个锁,去干别的事情去了。而lock则是没获取到锁时阻塞等待。
这就是ReentrentLock的基本用法,前面说过它还有更多synchronized没有的功能,例如唤醒和等待,而这些功能都在Condition方法里,他的唤醒可以指定唤醒,和wait,notify不同,notify是随机唤醒。虽然ReentrantLock有如此多的功能,但是它不如synchronize方便呀,平时加锁场景我们也不需要这么多功能,并且synchronized本身就是一个代码块加锁,不会出现忘记加unlock的情况。
2.信号量semaphore的使用
信号量就相当于是一个锁,但是他有n把钥匙,当超过n个线程需要钥匙时,需要锁的线程就会进入阻塞等待状态(WAITING从jconsole上可以得知),获取锁和释放锁就是PV操作,在Java中就是acquire和release。代码如下:
public class demo2 {//信号量Semaphore的使用
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(4);//创建一个拥有4个信号量的实例
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire()
System.out.println("获取锁");
}
}
接下来就是释放锁:
public class demo2 {//信号量Semaphore的使用
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(4);
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.acquire();
System.out.println("获取锁");
semaphore.release();
System.out.println("释放锁");
semaphore.acquire();
System.out.println("获取锁");
}
}
这样就不会阻塞了!!
3.CountDownLatch的使用
CountDownLatch相当于是一个线程任务完成计数器,当我们要将一个大任务分成若干个小任务时,就需要判定这些任务是否都已经完成,若所有任务都已经完成则进行下面的任务,就好比于多线程下载器,它就会分为多个线程去下载一个资源,全都下载完了再去把所有资源拼接成一个资源。这里我们在线程任务进行结束时可以使用CountDownLatch对象的countDown方法来表示这个任务已经结束,用CountDownLatch中的await方法来等待所有任务完成。在完成前主线程会处于阻塞等待状态。大致原理就是在计数器为0之前都阻塞等待。
public class demo3 {//CountDownLatch的使用
public static void main(String[] args) throws InterruptedException {
CountDownLatch count=new CountDownLatch(10);//表示一共有十个任务
for (int i = 0; i <10 ; i++) {
int id=i;
Thread.sleep(500);
Thread t=new Thread(()-> {
System.out.println("线程"+id+"已完成");
count.countDown();
});
t.start();
}
count.await();
System.out.println("任务已全部完成");
}
}
以上就是CountDownLatch工具的使用。
4.线程安全的集合类
平日我们使用的集合类(ArrayList,LinkedList等除了(Vector,Stack,HashTable))都是线程不安全的,所以我们可以在使用这些集合类的时候给集合类加个锁,如synchronized,当然不止这一种方法,Java还提供了一种套马甲的方式,使用Conllections工具,可以对一个List类套上线程安全的马甲,以下是代码:
public class demo4 {//线程安全的集合类使用
public static void main(String[] args) {
List<Integer> arrayList= Collections.synchronizedList(new ArrayList<>(10));
//使用synchronizedList方法,给他传递一个List对象,再用List接收即可。
}
}
因为他还是List类,所以使用方法和List方法一样。
public class demo4 {//线程安全的集合类使用
public static void main(String[] args) {
List<Integer> arrayList= Collections.synchronizedList(new ArrayList<>(10));
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
System.out.println(arrayList);
}
}
5.写时拷贝顺序表CopyOnWriteArraylist
当我们在对一个顺序表进行写操作时又需要对顺序表进行读操作,这就可能发生读操作读到了写到一半的数据,所以Java就实现了一个写时拷贝的顺序表,它的大致原理就是在写前拷贝一份新的顺序表,然后在新的顺序表上改动,改完之后再把老的顺序表复制成新的顺序表。这样读操作就只能读到写之前的操作或者写之后的操作,就不会读到写到一半的操作了。这只适合只有一个写线程并且顺序表不能很大。顺序表不能很大很好理解,因为要拷贝一份新的顺序表要花费很长时间,而只有一个写线程是因为写线程如果有多个,就可以发生两个写线程拷贝了同一份顺序表,然后再写顺序表时迟提交的顺序表就会覆盖掉早提交的顺序表。早提交的顺序表就白白操作了,如图:
CopyOnWriteArrayList的使用方法也是和ArrayList一样。我们看代码:
public class demo5 {//写时拷贝CopyOnWriteArrayList的使用
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> arrayList=new CopyOnWriteArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
System.out.println(arrayList);
}
}
上述就是CopyOnWriteArrayList的使用。
6.ConcurrentHashMap的使用和理解
ConcurrentHashMap时线程安全的HashMap。那又有一部分长得帅的读者就会说了“那HashTable不也是线程安全的嘛,你新建一种方法有什么用!你给我去Spa!!!”,我看你的完全不懂哦!
虽然它俩都是线程安全的,但是ConcurrentHashMap更加先进更加的快。因为HashTable只是相当于在HashMap上加了一个synchronized,但事实上HashMap的主要任务都在查上,写任务很少,所以在整个HashMap上添加一个synchronized无非是会大大程度地降低了程序运行的速度。所以Java意识到了这一点,在后续的Java中添加了一个新的线程安全的Map类ConcurrentHashMap。
那让我们来了解一下ConcurrentHashMap在哪优化了线程安全问题吧。
1.因为只有在相同的链表相邻的节点上上进行写操作才会有线程安全问题,但是我们不知道到底是在哪个节点上,所以Java就在每一条链表上都加上了锁,锁的类就是链头节点。
2.Java把一些不必要的加锁环节用CAS(CPU的compare and swap质量,原子性操作)来解决,如需要使用变量来记录hash中元素的个数。
3.Java做了一个很大胆的决定,他只给写和写加锁,不给读写,读度加锁,这是应为Java在底层编码上会尽量不使用++或是--操作,而是由赋值号=(原子性)来代替,所以读的时候要么是新值要么是旧值。
4.Java对HashMap的扩容也做出了优化,它不会一次性地把hashmap里的元素给拷贝到新的map中,而是新建一个Map在每一次put的时候都放一部分元素到新Map中,这样就可以使运行时不会突然一下子很慢一下子很快。而添加元素时直接添加在新map中,查找元素就在老map和新map中查找。
ConcurrentHashMap的使用方法其实也和HashMap一样,所以下面我们试着使用一下它:
public class demo6 {//ConcurrentHashMap的使用
public static void main(String[] args) {
ConcurrentHashMap<Integer,String> map=new ConcurrentHashMap<>();
map.put(1,"a");
map.put(2,"b");
map.put(3,"c");
map.put(4,"d");
map.remove(2);
System.out.println(map.get(1));
Iterator<Map.Entry<Integer,String>> it=map.entrySet().iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}
这就是本篇文章的所有内容啦,感谢大佬的观看,都看到这了,记得要点赞哈!!!