目录
前言:
Callable接口
代码实现
JUC下常见类使用
ReentrantLock类
代码实现
信号量
代码实现
CountDownLatch类
代码实现
线程安全的集合类
多线程环境下使用ArrayList
多线程环境下使用队列
多线程环境下使用哈希表
小结:
前言:
这篇文章主要介绍Callable接口,JUC包下一些常见类的使用,还有我们之前使用集合类在多线程环境下的使用。
Callable接口
可以使用Callable接口创建带有返回值的线程任务(和Runable类似)。这样的线程具有返回值,由于线程调度的随机性,我们不确定线程什么时候被调度,具体线程任务什么时候执行完毕。基于这样的问题采取FutureTask类对Callable进行包装。FutureTask就可以等待Callable的执行结果。
FutureTask提供的get方法就可以获取Callable的返回结果。它会阻塞,直到Callable里的任务执行完毕。
代码实现
public class ThreadDemo30 {
public static void main(String[] args) throws Exception {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
//get方法获取到结果,这里会进行阻塞,直到callable执行完毕,才能获取到结果
Integer tmp = futureTask.get();
System.out.println(tmp);
}
}
JUC下常见类使用
ReentrantLock类
ReentrantLock和Synchronized类似,都是加锁的。那么为什么存在Synchronized还要有ReentrantLock呢?下面介绍它的优点:
1)ReentrantLock的加锁和解锁是分开的,可以更加灵活的使用。
2)提供了公平锁的实现,只需要在构造方法中参数写为true即可。不写或者写false都是非公平锁
3)Synchronized产生的等待是死等,而它提供了tryLock()方法,返回值为boolean(是否加锁成功)。无参数版本能加锁就加不能则放弃。有参数版本可指定阻塞最大时间,如果时间到了不能获取到锁也就放弃了。
4)Synchronized加锁使用notify随机唤醒一个线程。它搭配Condition类指定唤醒某个线程。
ReentrantLock提供lock()方法加锁,unlock方法解锁。由于两者是分开的,如果加锁成功了,为了能保证unlock方法一定可以执行我们将其写在finally代码块中。
代码实现
public class ThreadDemo31 {
volatile private static int sum = 0;
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void func() {
try {
reentrantLock.lock();
for(int i = 0; i <= 100; i++) {
sum += i;
}
}finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
func();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
func();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum);
}
}
信号量
信号量为可用资源个数。如果信号量为0,继续申请可用资源个数,就会阻塞(信号量不能为负数)。锁就可以视为计数器为1的信号量(二元信号量)。
可用资源个数就是具体的一个数字。描述了这个信号量可以提供的最大可用资源的数量,每申请一个可用资源,其计数器就减1。释放一个可用资源计数器就加1。
Java中提供Semaphore类来实现信号量。acquire()方法申请信号量,release()方法释放信号量,构造方法传递一个参数就是初始化信号量个数。
代码实现
public class ThreadDemo32 {
public static void main(String[] args) throws InterruptedException {
//初始化信号量为3
Semaphore semaphore = new Semaphore(3);
//申请信号量(个数 -1),可以指定参数一次就申请多个
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
semaphore.release();
semaphore.acquire();
semaphore.acquire();
//释放信号量(个数 +1),可以指定参数一次就释放多个
//semaphore.release();
}
}
注意:可以清楚看见代码在阻塞当中。整个进程都没有结束。
CountDownLatch类
CountDownLatch类可以实现如果有10个线程,可以使另一个线程阻塞到这10个线程全部执行完毕。类似的场景比如跑步比赛,只有当最后一个人到达终点才算比赛结束。
构造方法提供一个参数,描述了具体任务的数量。countDown()方法来体现一个任务执行完毕,await()方法阻塞到初始任务数量全部执行完毕,那么意味着只有和任务数量一致那一次countDown()方法才起作用。
代码实现
public class ThreadDemo33 {
public static void main(String[] args) throws InterruptedException {
//具体有10个任务
CountDownLatch latch = new CountDownLatch(10);
for(int i = 0; i < 10; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//任务执行完成,调用countDown方法,表示任务执行完成
latch.countDown();
}
});
t.start();
}
//阻塞到10个任务全部执行完成,第10次调用countDown方法才起作用
latch.await();
System.out.println("Aaaaaa");
//使用场景跑步比赛
//等待所有人跑完才算结束
}
}
线程安全的集合类
Vector,Stack,HashTable是线程安全的,但是不建议使用。其他线程都是线程不安全的。
多线程环境下使用ArrayList
1)可以自己手动加锁,来保证线程安全。
2)使用Collections类下的synchronizedList静态方法,对ArrayList进行包裹。synchronizedList关键操作都带有synchronized。如下图源码可以清楚看见。
3)CopyOnWriteArrayList
CopyOnWriteArrayList即写时复制的容器。针对读数据不做任何工作。针对写操作,首先会拷贝一份新的ArrayList,在这份新的当中写(两个不同的对象不会存在线程安全问题)。如果在写的期间需要读,就读旧的ArrayList,当新的写完后在替换到旧的上面去(替换的本质就算引用之间的修改,原子的)。
优点:
写时拷贝,不需要加锁,就可以实现线程安全。代码效率高。
缺点:
它只适合数据量比较小的(拷贝需要时间),并且占用内存较多,新写的数据不能第一时间读取到。
多线程环境下使用队列
1)ArrayBlockingQueue
基于数组实现的阻塞队列。
2)LinkedBlockingQueue
基于链表实现的阻塞队列。
3)PriorityBlockingQueue
基于堆实现带有优先级的阻塞队列。
4)TransferQueue
最多只包含一个元素的阻塞队列。
多线程环境下使用哈希表
HashMap多线程环境下是不安全的。HashTable多线程环境下线程安全(给主要方法上加了一把大锁)。ConcurrentHashMap更优化的线程安全哈希表。
优化之处:
1)将HashTable的一把大锁改为了小锁
如果两个元素在同一个链表(树)上,多线程下是不安全的。但如果不在同一条链表(树)上多线程下是安全的(两个元素间没有联系)。HashTable不管在没在同一个链表上,锁是加在方法上的,只要调用了这样的方法就会阻塞,那么两个元素在不同的链表上也是会阻塞。ConcurrentHashMap锁是加在每条链表或者树的头节点上的。元素在不同链表上由于锁对象不同,则不会产生锁竞争。元素在同一条链表上,锁对象相同,则产生锁竞争。
2)针对读不加锁,针对写加锁
读和读之间没有锁竞争。写和写之间存在锁竞争。读和写之间不存在锁竞争,这样就可能造成脏读问题(读了一条不全的数据),基于这样的问题,这里的写操作设计为:volatile + 原子写操作。那么就只有写完才能读数据,就不会存在脏读问题。
3)充分使用CAS
充分使用CAS,进一步减少锁的数量。比如维护元素个数。
4)针对扩容采取“化整为零”的方式
HashMap/HashTable扩容时首先开辟一块更大的数组,将旧数组上数据重新哈希到新数组上,再释放旧数组。如果数据量较大,可能某次put操作就比较耗时。
ConcurruteHashMap如果需要扩容,首先开辟一块更大的数组,会每次搬运一小部分数据。保留新旧两个数组,put时就往新数组上哈希,并且搬运一部分旧数组数据到新数组上(后续只要操作ConcurruteHashMap的线程都会参与搬运的过程),删除旧数组上被搬运的元素。直到旧数组数据全部搬运完毕,释放旧数组。查找元素时两个数组都查找。删除元素时,两个数组也查找,找到就正常删除即可。
小结:
与大家分享一句名言:真正的才智是刚毅的志向。 ----- 拿破仑