前沿:
自己之前有所了解,但是因为没有怎么经常使用这个包下的相关类,这次将以前代码整理整理,做一些小demo,方便自己以后的查看,也希望看到这篇博客的盆友一个小小的帮助,如有错误,希望各位指出!
一、synchronized篇
synchronized并不是java.util.concurrent包下的,但是因为其java.util.concurrent包下很多跟线程有关,我这里把线程和锁的问题也整理下,如不需要可略过本节。
前沿: synchronized锁住的是代码还是对象????
答案是:synchronized锁住的是对象,而不是代码。对于非static的synchronized方法,锁的就是实例对象本身也就是this。对于static的synchronized方法🔒的是全局对象
synchronized从锁的是谁的维度一共有两种情况:
- 锁住类
- 锁住对象实例
1.1、问题
问题的起因?为什么要用synchronized,现在有一个需求同时有1000个线程,每次都需要增加count的数量,希望最后的结果是1000,代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count));
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
}
}
最后你会发现 控制台 并没有1000输出,甚至连999都莫得
原因就是 (++count) 这个操作并不是原子性的操作,也就是说他说分为两步骤的,那么如果一个线程在操作第一步,第二步增加还没有做的时候,其它线程过来了操作,获取新增,这时就会重复
1.2、synchronized method(锁方法-其实也是🔒实例对象)
所以想要保证在操作的过程中其它线程不能操作这个方法,我们可以在对应的方法上加上synchronized关键字保证如果有一个线程操作了这个方法,那么其它线程都必须等待当前线程执行完成这个方法,也就是所谓的给这个方法加锁,代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public synchronized void run() {
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count));
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
}
}
此时就会输出需要的数值了,但是你会发现最后输出1000的并不是第999个线程,因为你不能保证什么时候那个线程获取到了锁。
1.3、synchronized code block(锁类-同步代码块)
上面的是在方法上加上了synchronized,但有时候我们并不想那么做,什么意思呢?也就是说这个方法里面我们有一些代码是可以同时让多个线程访问的,并不需要考虑到一致性,比如,每次进入这个方法的时候,有一句话需要打印,这个是不要排队等待的。synchronized要按照同步代码块来保证线程安全,但是这就比加在方法“复杂”多了。有:
- synchronized(this){}这么写的
- 也有synchronized(Count.class){}这么写的
- 还有定义了一个private Object obj = new Object; ….synchronized(obj){}这么写的
1.3.1、synchronized(this)(🔒实例对象)
这个相信大家也都明白this,代表的是当前实例对象也是类似于(Demo01 demo01 = new Demo01();)这种,代码如下
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
System.out.println("hello 我不需要被锁住");
synchronized (this){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count));
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
}
}
因为这1000个线程都是使用同一个Demo01的实例所以,遇到synchronized还是需要排队等待。
1.3.2、synchronized(Demo01.class)(🔒实例对象)
虽然这1000个线程都是使用同一个Demo01的实例,但是Demo01.class🔒的还是实例对象 ,即遇到synchronized还是需要排队等待。代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
System.out.println("hello 我不需要被锁住");
synchronized (Demo01.class){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count));
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
}
}
1.3.3、synchronized(count)(🔒自定义的对象)
因为这个自定义的对象并没有使用static,其实还是🔒的是实例对象。代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
synchronized (count){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count));
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
}
}
1.3.4 小结
你会发现hello 我不需要被锁住这句话并不是和count等于几一起输出,而是无序的,也就说他不要跟count++那句操作一起进入等待排队。
1.4、锁实例与static区别
对于上面的情况,分别如下
- 对于1.3.1、synchronized(this) - 这个改不了,肯定是锁的实例对象,如果现在我们改变代码,使用2个不同的实例对象,分别用1000个线程操作count变量(不是static的count),肯定就是出现2个1000,因为,synchronized(this) 🔒住的是同一个实例对象,不能保证不同的实例对象!
代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
System.out.println("hello 我不需要被锁住");
synchronized (this){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count)+" "+this);
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
Demo01 demo02 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
for (int i=0;i<1000;i++){
new Thread(demo02,"当前线程"+i).start();
}
}
}
- 对于1.3.2、synchronized(Demo01.class)(🔒实例对象)。如果现在我们改变代码,使用2个不同的实例对象,分别用1000个线程操作count变量(不是static的count),肯定就是出现2个1000,和上面的情况一样,代码如下:
public class Demo01 implements Runnable{
private Integer count = 0;
@Override
public void run() {
System.out.println("hello 我不需要被锁住");
synchronized (Demo01.class){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count)+" "+this);
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
Demo01 demo02 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
for (int i=0;i<1000;i++){
new Thread(demo02,"当前线程"+i).start();
}
}
}
- 对于1.3.3、synchronized(count)(🔒自定义的对象)这个我们可以改成static的,其实这个就锁住了所有的实例对象,不论你new 多少个实例对象来,但是count为static就一份。如果现在我们改变代码,使用2个不同的实例对象,分别用1000个线程操作count变量,肯定就是出现1个2000,其它就是(1-2000区间),绝不会重复
public class Demo01 implements Runnable{
// 注意这里加了static
private static Integer count = 0;
@Override
public void run() {
System.out.println("hello 我不需要被锁住");
synchronized (Demo01.class){
System.out.println(Thread.currentThread().getName()+"-->> count= "+(++count)+" "+this);
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
Demo01 demo02 = new Demo01();
for (int i=0;i<1000;i++){
new Thread(demo01,"当前线程"+i).start();
}
for (int i=0;i<1000;i++){
new Thread(demo02,"当前线程"+i).start();
}
}
}
- 对于1.2 synchronized method(锁方法-其实也是🔒实例对象),我们将方法改成static 其实也就是锁住了很多实例对象了,其结果也是只有一个2000,就不展示图片了。
1.5、synchronized-常类池
当访问一个带锁的方法时,该对象的加了锁的方法都无法访问(得是同一个锁偶)。随便说明下常量池的问题。
public class Demo11 {
/**
* 此时 对象s1 和对象s2 是同一个对象
* 常量池
*/
String s1 = "s1";
String s2 = "s1";
void m1(){
synchronized (s1){
System.out.println("s1");
while (true);
}
}
void m2(){
synchronized (s2){
System.out.println("s2");
while (true);
}
}
public static void main(String[] args) throws InterruptedException {
/**
* s1 和 s2 是同一个对象,所以无法同时调用
* 因为在同一个对象时(其实就是同一个内存地址),当访问一个带锁的方法时,该对象的加了锁的方法都无法访问
* 之前有例子
*/
Demo11 demo11 = new Demo11();
new Thread(demo11::m1).start();
TimeUnit.SECONDS.sleep(2);
new Thread(demo11::m2).start();
// 即使你new 一个新的对象 也没用
// new Thread(new Demo11()::m2).start();
}
}
即使没有static,或者你new 对象,但还是会锁住,因为两者锁住的是同一片内存地址。
但是如果不是同一个锁,还是莫得用的。如下:s1后还是可以调用s2
public class Demo11 {
/**
* 此时 对象s1 和对象s2 是同一个对象
* 常量池
*/
String s1 = "s1";
String s2 = "s1";
Object o = new Object();
void m1(){
synchronized (s1){
System.out.println("s1");
while (true);
}
}
void m2(){
// 锁另一个对象
synchronized (o){
System.out.println("s2");
while (true);
}
}
public static void main(String[] args) throws InterruptedException {
Demo11 demo11 = new Demo11();
new Thread(demo11::m1).start();
TimeUnit.SECONDS.sleep(2);
new Thread(demo11::m2).start();
}
}
1.6、异常(⚡)
- synchronized 的锁🔒与spring的事务管理一样,一旦遇到异常,但是你又没有捕捉,那么就会释放对应的锁,但是如果你try…cacth…了则不会释放,默认你自己知道如何出处理,是否决定释放锁。
public class Demo06 {
private int i;
private synchronized void m1(){
while (true){
i++;
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// try {
if(i == 5){
i = i/0;
}
// }catch (Exception e){
// System.out.println("捕捉t1线程异常");
// }
}
}
public static void main(String[] args) {
Demo06 demo06 = new Demo06();
new Thread(demo06::m1,"t1").start();
new Thread(demo06::m1,"t2").start();
//注意偶 这里验证的时候不能用下面这种方式,因为你的synchronized 锁住的是实例对象
// 现命这个是等于又加了另一个实例对象,那个方法是锁不住的
// new Thread(new Demo06()::m1,"other").start();
}
}
- 这里我们先捕捉异常,那么在i等于5的时候就会抛出异常,t1线程就会释放对应的锁🔒,t2线程获取锁
- 此时我们捕捉的注释打开,会发现,即使异常发送了,但是t2线程还是没有运行。
二、volatile & native & transient 篇
2.1、volatile
volatile 并不保证原子性,保证所用的值直接从内存中取,而不是缓存,即cpu缓存刷新。
直接一个例子说明,大家就明白了。如下,一个线程根据变量b 判断释放继续死循环,但是我们在主线程(main)中在子线程2秒后将b变量变为false,想让子线程停止循环。如果没有加入volatile 那么子线程中会一直拿到的是b的缓存,即true,不会停止,但是加上volatile 后,在遇到b这个变量的时候,cpu会刷新缓存,获取最新的!
public class Demo07 {
/*volatile*/ Boolean b = true;
private void m1(){
System.out.println("start");
while (b){
System.out.println("running.....");
}
System.out.println("end");
}
public static void main(String[] args) {
/**
* volatile 并不保证原子性,保证所用的值直接从内存中取,而不是缓存即只能保证可见性
* 即cpu缓存刷新
*/
Demo07 demo07 = new Demo07();
new Thread(demo07::m1,"t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo07.b = false;
}
}
2.2、native
- native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
2.2、transient
-
我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
-
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
-
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
三、Atomic篇
如何在多线程的情况下保证数值的可靠性?可以使用synchronized篇的方法,但是其java.util.concurrent包下帮我们实现基础的类型,这里以AtomicInteger说明。
public class Demo09 {
/**
* 保证count的操作都是原子操作,
* 这样即使有多个线程操作它,也不会影响
* 因为比如 count++ 或者 count = count+1;都不是原子性操作
*/
AtomicInteger count = new AtomicInteger(0);
void m(){
for (int i=0;i<10000;i++){
// 相当于 ++i
count.incrementAndGet();
// i++
// count.getAndIncrement();
}
}
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>();
Demo09 demo09 = new Demo09();
for (int i=0;i<10;i++){
threadList.add(new Thread(demo09::m,"线程"+i));
}
threadList.forEach(Thread::start);
threadList.forEach(thread -> {
try {
// 等待该线程终止 ,即等到10个线程全部结束后主线程 才继续运行(也就是执行 System.out.println(demo08.count); )
// join()和wait()不会释放锁
// 可以参考 https://www.cnblogs.com/duanxz/p/5038471.html
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(demo09.count);
}
}
其中 thread.join()
- 主线程生成并启动了子线程,而子线程里要进行大量的耗时的运算
- 当主线程处理完其他的事务后,需要用到子线程的处理结果,这个时候就要用到join();方法了(子线程使用)
是不止AtomicInteger的 ,在 java.util.concurrent.atomic包下还有其它很多实现的,具体如下
四、countDownLatch篇
4.1、countDownLatch简单解释
countDownLatch顾名思义数量锁🔒,也就是可以给指定目标加上一定数量的锁,其代码例子看下面的,也就是说只有当**m2()方法执行countDownLatch.countDown()**将初始门闩上放的 5 把锁全部解开后(调用一次解开一把锁),**m1()的方法中的countDownLatch.await()**才会放开,不然会一直等待
public class Demo12 {
/**
* 初始将门闩上放 5 把锁
*
* 只对当前的实例对象有效果
* 如需要对所有实例都有效果 可以加上static
*/
/*static */CountDownLatch countDownLatch = new CountDownLatch(5);
void m1(){
try {
// 等待门闩上的锁全部打开
countDownLatch.await();
System.out.println("我m1又回来了!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2(){
for (int i =0;i<10;i++){
//当门闩上还有锁时
if(countDownLatch.getCount()!=0){
// 打开门闩上的锁,一次打开一把
countDownLatch.countDown();
System.out.println("门闩上还有"+countDownLatch.getCount()+"把锁");
}
}
}
public static void main(String[] args) {
Demo12 demo12 = new Demo12();
//只有门闩上的锁被全部打开了,m1的方法才可以执行
// CountDownLatch效率很高
new Thread(demo12::m1).start();
new Thread(demo12::m2).start();
}
}
执行结果如图:
4.2、使用
我们需要做出这样一个操作,有2个线程,一个向集合中添加数据,另一个线程判断这个集合中的元素数量达到一定数量后(处理对应的事件),这里集合数量我们假定给到5,对应的事件假定输出一句话。对于这个集合,我们可以简单自定义一下,很简单,就不多说了,如下:
public class MyArrayList<T> {
transient Object[] elementData;
private static final int DEFAULT_CAPACITY = 10;
private int size;
public MyArrayList(){
this.elementData = new Object[DEFAULT_CAPACITY];
}
public MyArrayList(Integer size){
this.elementData = new Object[size];
}
/**
* 扩容
*/
private void ensureCapacity(){
int newLength = this.elementData.length+( this.elementData.length>> 1);
this.elementData = Arrays.copyOf(elementData,newLength);
}
/**
* 增加元素
* @param o
*/
public void add(T o){
if(size>elementData.length-1){
ensureCapacity();
}
this.elementData[size++] = o;
}
public int getSize() {
return this.size;
}
}
4.2.1、使用whlie死循环检查
这个大家都能够想到,2个线程分别启动,检查数据的线程一致whlie(true){…}死循环检查集合数量,即可,但有一点需要注意,在死循环的时候,我们检查集合数量一定要是最新的,不能从缓存中拿,原因请参考上文的volatile篇。代码很简单,就不多说了。如下:
public class Demo01 {
/**
* 注意这里的volatile是重点,没有的话,是判断不了的
* 以后遇到 while (true) 时的多线程,相关变量最好加上volatile
* 以保证可见性,但是同时也会损耗一定的性能,这是不可避免的
*/
volatile static MyArrayList<Integer> myArrayList = new MyArrayList<>();
public static void main(String[] args) {
/**
* 第一种方式实现
*/
new Thread(() -> {
for (int i=0;i<10;i++){
myArrayList.add(i);
System.out.println("add "+ i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(() -> {
while (true){
if (myArrayList.getSize()==5){
System.out.println("容器中的元素已近5个了!!");
break;
}
}
},"t2").start();
}
}
结果:
4.2.2、使用synchronized锁🔒
这个也是,我们可以使用锁的wait方法,直到存放数据的线程,元素数量够了,使用notifyAll方法将其唤醒即可,代码如下:
public class Demo02 {
/**
* 这里没有 volatile
*/
static ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) {
/**
* 使用锁的wait 与notifyAll或者notify
* 当前线程必须拥有此 对象监视器 。
* 该线程发布对此监视器的所有权并等待 ,直到其他线程通过调用
* notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。
* 然后该线程将等到重新获得对监视器的所有权后才能继续执行。
*/
final Object lock = new Object();
/**
* 这里要特别注意一定要将await的线程先开启
* 再将notifyAll的线程开启
*/
new Thread(() -> {
// 必须要有 对象监视器 。synchronized (lock)
synchronized (lock){
try {
if(list.size()!=5) {
//先将当前线程阻塞
lock.wait();
}
System.out.println("容器中的元素已近5个了!!");
//再唤醒后面的线程
lock.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
new Thread(() -> {
// 必须要有 对象监视器 。synchronized (lock)
synchronized (lock){
for (int i=0;i<10;i++){
list.add(i);
try {
System.out.println("size "+ list.size());
TimeUnit.SECONDS.sleep(1);
if (list.size() == 5){
// 数量已经5个了唤醒前面的等待的线程
lock.notifyAll();
// 将当前线程阻塞
lock.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
},"t1").start();
}
}
4.2.2、使用countDownLatch锁🔒
对于上面的要求,我们配置对应的锁的数量,然后增加一个元素就减去一把锁,即可,如下:
public class Demo03 {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>();
// 在一个门闩上放5把锁 0 1 2 3 4
CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(() -> {
for (int i=0;i<10;i++){
list.add(""+i);
System.out.println(i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 增加一个元素 减少一把锁
if(countDownLatch.getCount()!=0){
countDownLatch.countDown();
}
}
},"t1").start();
new Thread(() -> {
try {
countDownLatch.await();
System.out.println("容器中的元素已近5个了!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
五、Lock篇
5.1、Lock之ReentrantLock(重入锁🔒)
- em…就不解释了,因为代码注解说明考虑一切,哈哈。
public class Demo01 {
/**
* 重入锁
*/
final Lock lock = new ReentrantLock();
void m1(){
try {
//相当于synchronized的开始
lock.lock();
for (int i =0;i<10;i++){
TimeUnit.SECONDS.sleep(1);
System.out.println("m1");
if(i ==5){
//当有异常出现,并且你没有catch则会自动释放改锁,与synchronized一样
i = i/0;
}
}
}catch (Exception e){
e.printStackTrace();
} finally{
// 相当于synchronized的结束 |
// 这里判断是 第五次会发生异常,释放锁,也可以根据对应的情况将锁🔒释放调
lock.unlock();
}
}
void m2(){
lock.lock();
System.out.println("m2");
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
/**
* lock的lock与unlock相当于synchronized锁住的代码块或者方法
* 当有异常出现,并且你没有catch则会自动释放该锁,与synchronized一样
*/
Demo01 demo01 = new Demo01();
new Thread(demo01::m1).start();
// 会一直等待 获取到锁为止
new Thread(demo01::m2).start();
// 但是同样 你如果new 了一个新的对象 也是锁不住的, 但是你可以给 Lock 对象加上static即可
// 这里等 2 秒 是因为 希望 m1 线执行上锁 🔒
// TimeUnit.SECONDS.sleep(2);
// new Thread(new Demo01()::m2).start();
}
}
5.2、Lock之ReentrantLock-tryLock方法
我们可以在尝试在一定事件内获取锁🔒失败后继续执行(tryLock方法),选择不等待锁,各位看官,请看下面这个列子:
public class Demo02 {
/**
* 重入锁
*/
final Lock lock = new ReentrantLock();
void m1(){
try {
lock.lock();
for (int i =0;i<10;i++){
TimeUnit.SECONDS.sleep(1);
System.out.println("m1");
}
}catch (Exception e){
e.printStackTrace();
} finally{
System.out.println("m1释放锁");
lock.unlock();
}
}
void m2(){
boolean isLock = false;
try {
//在5秒内尝试获取锁,在指定时间内获取不到就拉倒 不会一直等待
// 会继续执行
isLock = lock.tryLock(5, TimeUnit.SECONDS);
if (isLock){
System.out.println("以获取锁");
}else {
System.out.println("没有获取锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (isLock){
lock.unlock();
}
}
}
public static void main(String[] args) {
Demo02 demo02 = new Demo02();
/**
* 在本代码中 如果当前对象被锁住了(因为m1中使用了lock的lock方法大约10秒)
* 相当于synchronized锁住了当前实例对象10秒
* 锁在m1上。m2就无法获取到锁,但是我们可以在尝试在一定事件内获取锁🔒失败后继续执行,选择不等待锁
*/
new Thread(demo02::m1).start();
new Thread(demo02::m2).start();
}
}
5.3、Lock之ReentrantLock-lockInterruptibly方法
强行打断持有锁的线程,并将锁拿到自己这里,各位看官,请看下面这个列子,其中也说明了一些其它知识点。
public class Demo03 {
final Lock lock = new ReentrantLock();
void m1(){
try {
lock.lock();
for (int i = 0;i<5;i++){
TimeUnit.SECONDS.sleep(1);
System.out.println("m1 ");
}
}catch (Exception e){
System.out.println("m1 exception");
e.printStackTrace();
}finally {
lock.unlock();
}
}
void m2(){
try {
/**
* 强行打断持有锁的线程,并将锁拿到自己这里
* 所以被强行打断的线程可能会抛出异常
*/
lock.lockInterruptibly();
System.out.println("m2 get lock");
} catch (InterruptedException e) {
System.out.println("m2 exception");
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
Demo03 demo03 = new Demo03();
new Thread(demo03::m1,"t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(demo03::m2);
t2.start();
/**
* 新生(new) -> 就绪(start,) -> 运行(cpu开始调度该线程) -> (结束)
* - -
* - -
* 阻塞(sleep,wait)
*
* 阻塞状态
* - 普通阻塞:
* sleep(1000) 可以被打断,调用thread.interrupt方法,但会抛出异常
* - 等待队列
* wait()方法被调用,不可以被打断,只能调用notify方法唤醒
* - 锁池队列
* 不是所有的锁池队列都可被打断
*/
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
}
5.4、Lock之ReentrantLock-开启公平锁🔒
如果2个线程同时进行,并不会2个线程轮流执行,但是ReentrantLock的构造器中有一个参数,为true,表示开启公平,各位看官,请看下面这个列子。
public class Demo04 {
public static void main(String[] args) {
// FairReentrantLock lock = new FairReentrantLock();
SyncLock lock = new SyncLock();
new Thread(lock,"t1").start();
new Thread(lock,"t2").start();
}
}
class FairReentrantLock extends Thread{
// 开启公平锁(true),根据线程的等待时间,越长的先调用,改变默认的性能优先的策率
// 这里就是两个线程轮流获取资源
// 如果没有true,那么就和synchronized一样
final ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
for (int i=0;i<5;i++){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" get "+i);
}finally {
//一定要在finally中将锁释放
lock.unlock();
}
}
}
}
class SyncLock extends Thread{
// synchronized是非公平的,即使你锁住的是如下这样,还是不行
@Override
public void run() {
for (int i=0;i<5;i++){
// 这里需要做的不是先执行完 t1 再执行t2 而是想要将
// t1 和 t2 公平 的进入这里后 依次输出 t1 t2
// 不能将 synchronized 放到for 循环外面,否则就没有效果了 -- 这里是重点偶
synchronized (this){
System.out.println(Thread.currentThread().getName()+" get "+i);
}
}
}
}
当使用SyncLock lock = new SyncLock();时,几乎都是t1线执行完,再执行t2
当使用FairReentrantLock lock = new FairReentrantLock 时,t1和t2轮流输出
六、BlockingQueue-阻塞队列篇
-
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
-
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。
-
负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
-
其官方具体实现有这么多,这里我就不挨个说明官方的了,大家可以直接google一下即可。后面主要说下自定义的实现
前提如下:
- 自定义同步容器,容器上限为10,可以在多线程中应用,并保证数据安全。
也就是说,当容器中已经有了10个元素,那么任何线程都不可以再向里面增加数据了,必须在外等待,直到有线程取出数据,才可以增加 - 联系(生产者与消费者模式)
6.1、阻塞队列-wait-notifyAll-实现
public class Demo01 {
public static void main(String[] args) {
Container01<Integer> container = new Container01<>();
//生产者
for (int i=0;i<2;i++){
new Thread(() -> {
for (int j=0;j<20;j++){
container.put(j);
}
},"生产者"+i).start();
}
//消费者
// new Thread(() -> {
// for (int j=0;j<40;j++){
// container.get();
// }
// }).start();
}
}
class Container01<T> {
final List<T> container = new LinkedList<>();
private static final Integer CAPACITY = 10;
void put(T o){
/**
* 使用synchronized来保证当一个线程在执行操作的时候,其它线程无法操作
* 而且使用wait与notify必须要使用synchronized
* 这里最需要最需要注意的是 使用 while (container.size()==CAPACITY)判断,而不要使用if
* if有时可能会出现多线程判断错误的情况
*/
synchronized (this){
while (container.size()==CAPACITY){
try {
//进入等待队列,将锁给别人,等待容量少于10 时
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
container.add(o);
System.out.println("container add "+ o);
//通知容器里已经进去一个了
this.notifyAll();
}
}
void get(){
synchronized (this){
while (container.size()==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
container.remove(container.size()-1);
//通知容器里已经出来了一个
this.notifyAll();
}
}
}
6.2、阻塞队列-countDownLatch-实现
public class Demo02 {
public static void main(String[] args) {
Container02<Integer> container02 = new Container02<>();
//生产者
for (int i=0;i<2;i++){
new Thread(() -> {
for (int j=0;j<20;j++){
container02.put(j);
}
},"生产者"+i).start();
}
//消费者
// new Thread(() -> {
// for (int j=0;j<1;j++){
// container02.get();
// }
// }).start();
}
}
class Container02<T>{
private final List<T> container = new LinkedList<>();
private static final Integer CAPACITY = 10;
CountDownLatch countDownLatch = new CountDownLatch(1);
void put(T o){
while (container.size()==CAPACITY){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
container.add(o);
System.out.println("container add "+ o);
if(countDownLatch.getCount()>=1) {
countDownLatch.countDown();
}
}
T get(){
while (container.size()==0){
try {
countDownLatch.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T remove = container.remove(container.size() - 1);
if(countDownLatch.getCount()>=1) {
countDownLatch.countDown();
}
return remove;
}
}
6.3、阻塞队列-ReentrantLock-实现
public class Demo03 {
public static void main(String[] args) {
/**
* 使用reentrantLock的条件(Condition)就可以替代demo01中的synchronized和wait了
*/
Container03<Integer> container03 = new Container03<>();
//生产者
for (int i=0;i<2;i++){
new Thread(() -> {
for (int j=0;j<20;j++){
container03.put(j);
}
},"生产者"+i).start();
}
//消费者
// new Thread(() -> {
// for (int j=0;j<5;j++){
// container03.get();
// }
// }).start();
}
}
class Container03<T> {
final List<T> container = new LinkedList<>();
private static final Integer CAPACITY = 10;
final Lock lock = new ReentrantLock();
Condition producer = lock.newCondition();
Condition consumer = lock.newCondition();
void put(T o){
try {
lock.lock();
while (container.size()==CAPACITY) {
//生产者进入等待队列,将锁给别人
producer.await();
}
container.add(o);
System.out.println("container add "+ o);
//通知容器里已经进去一个了,消费者可以使用了
consumer.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
void get(){
try {
lock.lock();
while (container.size()==0){
//消费者进入等待队列,将锁给别人
consumer.wait();
}
container.remove(container.size()-1);
//通知容器里已经出来了一个,生产者可以生产了
producer.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
6.4、阻塞队列-实现-统一测试
验证上面的需求,我们只需要把消费者注释调,只开启生产者,会输出,10个数据,但就会卡住,一旦我们开始消费者,那么会一遍消费一遍生产。
6.5、小结
其实如下面这张图片所说,小结如下:
6.5.1、普通同步容器
- 解决并发情况下的容器线程安全问题,给多线程一个线程安全的容器对象
- 线程安全容器对象:
- Vector
- HashTable
但是其底层都是使用synchronized来实现的,还要通过moninter,entityList等等,效率不高
6.5.2、concurrent包中的同步容器
大多数都是系统底层技术代码实现线程安全,类似与native,,java8中的CAS等等,效率会更好。
6.5.3、 MAP/SET
- ConcurrentHashMap
- ConcurrentHashSet
底层hash实现的同步,效率高,线程安全。key和value不能为null
- ConcurrentSkipListMap
- ConcurrentSkipListSet
底层使用跳表实现,效率比ConcurrentHashMap略低。存储是排序的。
6.5.4、 2.2 List
- Vector
底层使用synchronized实现
- CopyOnWriteArrayList
线程安全,但是写数据太慢,因为每次写都要新建,一个底层数组再copy一份前面的列表,但是读就很快。
6.5.5、 queue
-
无论
队列
还是栈
,都可以使用数组
或者链表
实现,区别就在于FIFO还是FILO -
BlockingQueue是阻塞的(Blocking)并且是一个接口,所以所有子类,都有阻塞的方法(put,take)也就会抛出InterruptedException异常
- ConcurrentLinkedQueue
- LinkedBlockingQueue
- ArrayBlockingQueue
- DelayQueue
- TransferQueue
- SynchronousQueue
关于这部分的简单性能验证问题,大家可以自己测试下,代码我就不放上来了,就是循环向对象中添加元素,计算下时间。
这里举个部分例子看下:
public class Demo01_map_set {
/**
* 线程的个数 | 要保证线程个数和门闩的个数一样
*/
private static final Integer THREAD_NUM = 100;
/**
* HashTable是线程安全的,这里没有拿HashMap来说,因为它线程不安全
* 底层使用的是synchronized来实现的
* 共消耗20300毫秒,最总map的大小为10000000
*/
static Hashtable<String,String> MAP2 = new Hashtable<>();
/**
* 共消耗31301毫秒,最总map的大小为10000000
* 底层不是synchronized实现的
*/
static ConcurrentHashMap<String,String> MAP1 = new ConcurrentHashMap<>();
/**
* 共消耗13430毫秒,最总map的大小为10000000
*
* 请参考 https://juejin.im/post/5d9beab85188251d805f3f6c
*/
static ConcurrentSkipListMap<String,String> MAP = new ConcurrentSkipListMap<>();
public static void main(String[] args) {
/**
* 100个线程
* 每个线程向同一个map实例对象中放入1000个数据
* 计算所花费时间
*
* 门闩的作用是为了防止主线程(即Main线程)先跑到end位置输出结果,再等待其它100线程结束
*/
final CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
Thread []threads = new Thread[THREAD_NUM];
Random random = new Random();
long start = System.currentTimeMillis();
for (int i=0;i<THREAD_NUM;i++){
threads[i] = new Thread(() -> {
for (int j=0;j<100000;j++){
MAP2.put(Thread.currentThread().getName()+j+random.nextInt(Integer.MAX_VALUE),1+"");
}
countDownLatch.countDown();
});
}
for (Thread t : threads){
t.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(String.format("共消耗%d毫秒,最总map的大小为%s",(end-start),MAP2.size()));
}
}
七、 线程池
这部分也就不上代码示例了,主要这部分大家平时使用的话可能会很多,估计都多多少少知道,就不上了。
-
Executor
-
Executors.xxxx (快速创建各种threadPool)
-
ExecutorService
- execute
- void runnable 无返回值
- submit
- void runnable
- future callable 返回值为future
- execute
-
Executors.newFixedThreadPool
首选推荐使用,指定线程池数量,即使线程空闲也不会回收 -
Executors.newCachedThreadPool
代码将newFixedThreadPool换成newCachedThreadPool即可
缓存线程池,容量不限(Integer.MAX_VALUE),自动扩容,默认线程
空闲60秒(当然是可以自己指定的),自动销毁
场景: 测试出最好的线程池中的线程数量,为newFixedThreadPool
做铺垫 -
Executors.newScheduledThreadPool
这个可以上一下代码:如下
public class Demo04 {
public static void main(String[] args) {
/**
* 指定线程池中的初始数量
* 计划任务线程池,可以根据计划自动执行线程池中任务,
* 底层就是delayQueue(处理5分钟内未支付的订单)
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
/**
* runnable ----> 需要执行的任务
* start_limit ----> 开始第一次任务的时间间隔
* limit ----> 以后每次任务之间的间隔
* timeUnit ----> 时间单位
*/
scheduledExecutorService.scheduleAtFixedRate(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
},0,2, TimeUnit.SECONDS);
}
}
- Executors.newSingleThreadExecutor
就是一个线程池中只有一个线程可以用,不会增加,也不会减少
场景:单例模式中 - ForkJoinPool forkJoinPool = new ForkJoinPool(5)