Java高级多线程笔记
引言:
本文主要分享了多线程中的线程池概念(原理以及咋获取线程池)、Callable接口、Future接口、线程的同步以及异步、ReentrantLock(重入锁)、ReentrantRaedWriteLock(读写锁)
- Java多线程超详细笔记:分享了多线程的实现方式、7种状态、线程安全的两种解决方案以及死锁、生产者消费者问题;
- Java中线程安全的集合:分享了Collection体系集合下除了Vector以外的线程安全集合,包括:Collection中的安全工具方法、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、Queue接口
@[toc]
1. 线程池
池就是为了提供复用性,给出一个池实现复用;比如打客服电话,如果该客服占线需要等待,直到你之前的所有排在该客服的客户都咨询完你才可以咨询;客服这就相当于一个池,每天会接不同的电话而不是一个;
1.1 线程池引入
现有问题:
线程是宝贵的内存资源、单个线程约占1MB空间,过分分配易造成内存溢出。
频繁的创建及销毁线程会增加虚拟机回收频率、资源开销、造成程序性能下降。
线程池:
线程是容器,可设定线程分配的数量上限也就是同一时间并发执行的线程的个数。
将预先创建的线程对象存入池中,并实现重用线程池中的线程对象。
可以避免频繁的创建和销毁。
1.2 线程池原理
将任务提交给线程池,由线程池分配线程、运行任务、并在当前任务结束后复用线程。
如:有三个线程池,四个等待对象;先执行三个,当这三个中的一个执行完毕后空出线程池位置,第四个开始执行。
1.3 获取线程池
常用的线程池接口和类:在java.util.concurrent;包内。
Executor:线程池的顶级接口,执行已经提交的Runnable对象。
ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码。
Executors工厂类:通过此类可以获得一个线程池
通过newFixedThreadPool(int nThreads):获取固定数量的线程池。参数:指定线程池中线程的数量。
通过newCachedThreadPool():获得动态数量的线程池,如不够则创建新的,没有上限。
public class TestThreadPool {
public static void main(String[] args) {
//线程池(引用) ---->Executors工具类(工厂类)
ExecutorService es = Executors.newFixedThreadPool(3);//手动限定线程池里的线程数量3。
// ExecutorService es = Executors.newCachedThreadPool();//动态数量的线程池
//1.创建任务对象
MyTask task = new MyTask();
//2.将任务提交到线程池,由线程池调度、执行
es.submit(task);//Runnable类型的对象
es.submit(task);
es.submit(task);
es.submit(task);
es.submit(task);
}
}
//线程任务
class MyTask implements Runnable{
public void run() {
for(int i = 1;i<=5;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
运行结果:
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-3:1
pool-1-thread-3:2
pool-1-thread-3:3
pool-1-thread-3:4
pool-1-thread-3:5
pool-1-thread-2:1
pool-1-thread-2:2
pool-1-thread-3:1
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-3:2
pool-1-thread-3:3
pool-1-thread-3:4
pool-1-thread-2:3
pool-1-thread-2:4
pool-1-thread-2:5
pool-1-thread-3:5
- 以上程序是获取固定数量的线程池,由运行结果可以看出线程3最先执行完,之后有开始从1执行,实现了复用;
- 获取动态数量的线程池可以自动添加线程池,对内存要求较高;
2. Callable接口
JDK5之后加入,与Runnable接口类似都是为了变成任务被线程执行的对象,实现之后代表一个线程任务。Callable具有泛型返回值、可以声明异常,Runnable不行,没有返回值且无法抛出有返回值的异常。
Public interface Callable< V >{//V:value
Public V call() throws Exception;//计算结果,如无法计算则抛出
}
public class TestCallable {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(3);
MyTask task = new MyTask();
es.submit(task);
}
}
class MyTask implements Callable<Integer>{
@Override
public Integer call() throws Exception {
for(int i = 1;i<=10;i++) {
if(i ==5) {
Thread.sleep(3000);
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
return null;
}
}
3. Future接口
异步接收ExecutorServices.submit():所返回的状态结果,当中包含了call()返回值。
- V get():以阻塞形式等待Future中的异步处理结果(call()的返回值)
需求:
使用两个线程,分别计算1~50,51~100的和,进行相加
public class TestFuture {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService es = Executors.newFixedThreadPool(2);
MyCall mc = new MyCall();
MyCall2 mc2 = new MyCall2();
//通过submit执行提交的任务,Future接受返回的结果
Future<Integer> result = es.submit(mc);
Future<Integer> result2 = es.submit(mc2);
//通过Future的get方法,获得线程执行完毕后的结果
Integer value = result.get();
Integer value2 = result2.get();
System.out.println(value+value2);
}
}
//计算1~50的和
class MyCall implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for(int i = 1;i<=50;i++) {
sum = sum + i;
}
return sum;
}
}
//计算51~100的和
class MyCall2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Integer sum = 0;
for(int i =51;i<=100;i++) {
sum = sum + i;
}
return sum;
}
}
4. 线程的同步异步
4.1 线程同步
形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续。
主线程调用m1()方法,优先执行完m1()方法回到主线程调用m2()方法,优先执行完m2()方法回到主线程调用m3()方法,m3方法执行完毕后回到主线程结束
- 单条执行路径
4.2 线程异步
形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知之后立刻返回。二者竞争时间片,并发执行;
主线程开始执行,告知m1之后回到主线程,m1也开始执行,各执行各的,二者共同竞争时间片;
- 多条执行路径
5. Lock接口
JDK5提供了一个比synchronized更广泛的锁定操作(接口:Lock),可以清晰的看到在哪里加了锁;
- public void lock():加锁
- boolean tryLock()//尝试获取锁(成功返回true失败false,不会阻塞)仅在空闲时候才获取锁
- public void unlock():试图释放锁
重入锁
也称递归锁,最多支持同一个线程发起的2147483648个递归锁,超过此限制就会抛出error错误;
Lock无法实例化只能借助子实现类:ReentrantLock可重入锁在 java.util.concurrent.locks包下 ;
public class TestLocks {
public static void main(String[] args) {
Test obj = new Test();//加锁
Thread t1 = new Thread(new MyTask(obj));
Thread t2 = new Thread(new MyTask2(obj));
t1.start();
t2.start();
}
}
class Test{
//创建Lock锁对象
Lock lock = new ReentrantLock();
public void method() {
System.out.println(Thread.currentThread().getName()+"进入到上锁的方法里");
try {
//显示的写上 在此处获得锁
lock.lock();
try{
Thread.sleep(3000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"退出了上锁的方法里");
}finally {
//显示的写上,此处释放锁
lock.unlock();
}
}
}
class MyTask implements Runnable{
Test obj;//共享资源对象
public MyTask(Test obj) {
this.obj = obj;
}
public void run() {
obj.method();
}
}
class MyTask2 implements Runnable{
Test obj;//共享资源对象
public MyTask2(Test obj) {
this.obj = obj;
}
public void run() {
obj.method();
}
}
运行结果:
Thread-0进入到上锁的方法里
Thread-1进入到上锁的方法里
Thread-0退出了上锁的方法里
Thread-1退出了上锁的方法里
- 谁先退出谁先执行,如果没有lock.unlock();则不会释放锁;
注:
- 当使用Lock时,需要明确的写上锁和释放锁
- 为了避免拿到锁的线程在运行时出现异常,导致程序终止没有释放锁;应用try{}finally{}保证最后会释放锁。
- 设置正确的出口,使得每把锁都释放,否则会出现(StackOverflowError内存溢出错误)
6. 读写锁
ReentrantRaedWriteLock:
- 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
- 支持多次分配读锁,使多个读操作可以并发执行。
读锁:ReadLock
写锁:WriteLock
- 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率;
isShutdown():如果程序关闭返回true
isTerminated():如果关闭后所有任务都完成,返回true
shutdown():启动一次顺序关闭,执行以前提交的任务,不接受新任务
public class TestReadWriteLock {
public static void main(String[] args) {
Student stu = new Student();//共享资源对象
ExecutorService es = Executors.newFixedThreadPool(20);//20个线程池
WriteTask write = new WriteTask(stu);//写线程任务
ReadTask read = new ReadTask(stu);//读线程任务
long start = System.currentTimeMillis();//开始时间 毫秒值
es.submit(write);
es.submit(write);
for(int i =1;i<=18;i++) {
es.submit(read);
}
es.shutdown();//停止线程池,但不停止已经提交的任务
while(true) {
System.out.println("结束了吗?");
if(es.isTerminated() == true) {
//线程池里的任务都执行完毕了
break;
}
}
long end = System.currentTimeMillis();//结束时间
System.out.println(end - start);
}
}
//写
class WriteTask implements Callable{
Student stu;
public WriteTask(Student stu) {
this.stu = stu;
}
@Override
public Object call() throws Exception {
stu.setAge(100);
return null;
}
}
//读
class ReadTask implements Callable{
Student stu;
public ReadTask(Student stu) {
this.stu = stu;
}
@Override
public Object call() throws Exception {
stu.getAge();
return null;
}
}
class Student{
private int age;
//Lock lock = new ReentrantLock();//重入锁
ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();//有两把锁
ReadLock read = rrwl.readLock();//读锁
WriteLock write = rrwl.writeLock();//写锁
//赋值---写操作
public void setAge(int age) {
write.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.age = age;
}finally {
write.unlock();
}
}
//取值---读操作
public int getAge() {
read.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.age;
}finally {
read.unlock();
}
}
}
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
结束了吗?
3013
- 使用Lock写2秒,读18秒
- 使用ReentrantRaedWriteLock,2秒写1秒读,共三秒如上,因为读操作时是异步的
互斥规则:
写-写:互斥、阻塞;
读-写:互斥、读阻塞写,写阻塞读;
读-读:不互斥,不阻塞;