Java多线程
Posted 流光之中
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程相关的知识,希望对你有一定的参考价值。
Java多线程
1. 线程与进程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
区别:
线程 | 进程 | |
---|---|---|
地址空间 | 共享本进程的地址空间 | 进程之间是独立的空间 |
资源 | 共享本进程的资源(内存,I/O,CPU等) | 进程之间的资源是独立的 |
执行过程 | 线程不能独立执行,必须存在于应用程序中,执行开销小 | 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口、执行开销大 |
并发性 | 可并发执行 | 可并发执行 |
调度和分配的基本单位 | 拥有资源的基本单位 |
2. Java中的线程
Java中创建线程的两种方式
-
继承Thread类并重写run()方法
/** * 测试扩展Thread类实现的多线程程序 */ public class TestThread extends Thread { public TestThread(String name){ super(name); } @Override public void run() { for(int i=0;i<5;i++){ for(long k=0;k<100000000;k++); System.out.println(this.getName()+":"+i); } } public static void main(String[] args){ Thread t1=new TestThread("李白"); Thread t2=new TestThread("屈原"); t1.start(); t2.start(); } }
-
通过实现Runnable接口创建
/** * 实现Runnable接口的类 */ public class RunnableImpl implements Runnable{ private Stringname; public RunnableImpl(String name) { this.name = name; } @Override public void run() { for (int i = 0; i < 5; i++) { for(long k=0;k<100000000;k++); System.out.println(name+":"+i); } } } /** * 测试Runnable类实现的多线程程序 */ public class TestRunnable { public static void main(String[] args) { RunnableImpl ri1=new RunnableImpl("李白"); RunnableImpl ri2=new RunnableImpl("屈原"); Thread t1=new Thread(ri1); Thread t2=new Thread(ri2); t1.start(); t2.start(); } }
3. 线程状态转换
- 新生状态:线程对象已创建还未执行start()
- 就绪态:线程有资格运行,等待调度程序调用其start()方法
- 运行态:调度程序从就绪态池中选取一个线程执行
- 阻塞态:线程是活的,但此时没有资格运行,阻塞接触后重新进入就绪态
- 死亡:代码执行完毕或者线程中断执行,一旦线程死亡,不可再执行start()
线程离开运行态:
- sleep() 线程睡眠
- yield() 线程让步,让其他相同优先级的线程优先执行
- join() 线程合并,使当前线程停止执行,让新加入的线程先执行完毕
- wait() 在对象上调用该方法
4. 同步和锁定
锁的原理:
Java每个对象都有一个内置锁,当程序运行到非静态的synchronized()方法/synchronized()代码块上时,自动获得当前实例(this)的锁,一个对象只有一个锁,所以当一个线程获得锁之后,其他线程都不能进入该实例的synchronized()方法/synchronized()代码块,直到该锁被释放。释放锁是指持锁线程退出了synchronized同步方法/代码块。
note:
- 不必同步类中的所有方法,类可以同时拥有同步方法和非同步方法
- 线程访问类中的非同步方法不受限制
- 线程睡眠不会释放锁
静态方法同步:需要一个应用于整个类对象的锁
public staticsynchronized int setName(String name){
Xxx.name = name;
}
//等价
public static intsetName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}
调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。
5. Java线程交互
void notify()——唤醒在此对象监视器上等待的单个线程。
void notifyAll()——唤醒在此对象监视器上等待的所有线程。
void wait()——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。
6. 线程的调度
-
休眠
Thread.sleep(long millis) Thread.sleep(long millis, int nanos)
线程休眠,会将CPU资源交给其他线程,但不会释放锁,以便线程可以轮换执行,休眠一定时间后,会再次进入就绪态
-
优先级
Thread.setPriority(10);
线程的优先级无法保证线程的执行顺序,但是优先级较高的线程获取资源的概率较大,线程优先级默认为5
在父线程中开启子线程,子线程的优先级会继承父线程的优先级
-
让步
Thread.yield()
线程让步的意思是当前线程暂停执行,让出CPU资源,并执行其他线程,但并不能指定是哪个线程,yield()是静态方法
-
合并
void join()——等待该线程终止。 void join(longmillis)——等待该线程终止的时间最长为 millis毫秒。 void join(longmillis,int nanos)——等待该线程终止的时间最长为 millis毫秒 + nanos 纳秒。
将几个并行的线程合并为一个单线程执行,当前线程会暂停执行,知道新加入的线程执行完毕
-
守护线程
public final void setDaemon(boolean on)--将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。
调用该方法可以将线程设置为守护线程,必须在启动前调用
JVM的垃圾回收,内存管理,操作系统的线程很多都是守护线程
JRE判断程序是否执行完毕的标准是所有的前台线程执行完毕,而不管后台线程的状态
-
同步方法
synchronized 关键词修饰方法
synchronized只能标记非抽象的方法,不能标识成员变量。
-
同步块
synchronized 关键字修饰代码块
在同步方法或同步代码块中应该尽可能减少使用sleep()和yield(),使用sleep()的话,该方法占着对象锁,其他线程无法访问该资源,使用yield()时,其他线程也无法访问同步方法或者同步代码块。
-
生产者消费者模型
-
死锁
发生死锁的原因:两个对象的锁互相等待
-
volatile关键字
volatile可以用在变量前面,防止两个线程同时操作数据引起其中一个线程读取到脏数据的情况
volatile的同步性比较差,但是其开销较低
-
线程池
线程池:在一块内存空间内,存放未死亡的线程,线程调度由池管理器管理。可以将常用的一些线程存放在线程池中,当有线程任务时,直接在线程池中取出,执行完成后放回线程池,可以节省反复创建相同线程的时间和空间开销,节省资源。
-
固定大小的线程池
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Java线程:线程池 */ public class Test { public static void main(String[] args) { //创建一个可重用固定线程数的线程池 ExecutorService pool =Executors.newFixedThreadPool(2); //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口 Thread t1 = new MyThread(); Thread t2 = new MyThread(); //将线程放入线程池中进行执行 pool.execute(t1); pool.execute(t2); //关闭线程池 pool.shutdown(); } } class MyThread extends Thread{ public void run(){ System.out.println(Thread.currentThread().getName()+"正在执行..."); } }
-
单任务线程池
ExecutorService pool=Executors.newSingleThreadExecutor();
单任务线程池中只有一个线程,当需要反复执行一个线程时,避免重复新建该线程,可以用线程池实现
-
可变大小的线程池
ExecutorService pool=Executors.newCachedThreadPool();
可以根据需要改变大小的线程池
-
延迟连接池
在给定延迟后运行命令或者定期地执行
ScheduledExecutorServicepool=Executors.newScheduledThreadPool(2); pool.schedule(thread, 10, TimeUnit.MILLISECONDS);
创建一个单任务执行线程池,它可安排在给定延迟后运行命令或者定期地执行
ScheduledExecutorServicepool=Executors.newSingleThreadScheduledExecutor();
自定义线程池
/* * corePoolSize : 池中所保存的线程数,包括空闲线程 * maximumPoolSize : 池中允许的最大线程数 * keepAliveTime : 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间 * unit -keepAliveTime : 参数的时间单位 * workQueue : 执行前用于保持任务的队列 */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
-
锁对象
Java5以后提供了锁对象,用来控制需要并发访问的资源
主要有三个接口:
java.util.concurrent.locks.Condition//替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。 java.util.concurrent.locks.Lock//实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作 java.util.concurrent.locks.ReadWriteLock//维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作
Lock myLock=new ReentrantLock(); ReadWriteLock myRWLock=new ReentrantReadWriteLock();//读写锁 @Override public void run() { //获取锁 myLock.lock(); //锁定对象 //释放锁 myLock.unlock(); //获取锁 myRWLock.readLock().lock(); //只能进行读操作 //释放锁 myRWLock.readLock().unlock(); //获取锁 myRWLock.writeLock().lock(); //只能进行写操作 //释放锁 myRWLock.writeLock().unlock(); }
-
信号量
信号量是一个计数器,可以监控有多少数目的线程等待获取资源,通过信号量可以得知可用资源的数目,但仅仅只能获取到数目,不能知道具体是哪些资源
-
阻塞队列
java.util.concurrent.BlockingQueue
指定长度的队列,队列如果满了,添加进队列的操作会阻塞,知道队内元素出队
-
阻塞栈
java.util.concurrent.BlockingDeque
指定长度的栈,栈如果满了,添加进栈的操作也会被阻塞,与阻塞队列不同的是,栈是先进后出的,队列是先进先出的
-
Java多线程 2.线程安全
2.1 资源共享导致线程安全
2.1.1 多线程、并行、并发
多线程指一个进程中启动了不止一个线程;
并行(concurrent)指不同的线程执行相同的代码,类似不同的人干相同的事;
并发(parallel)指不同的线程执行不同的代码,类似不同的人干不同的事;
无论是并行还是并发都可能产生不同线程同时访问相同的资源的情况,线程安全就产生于不同的线程同时访问相同的资源,资源可以是数据库、IO、内存,本章涉及的资源都是内存;
2.1.2 JVM内存分布
不同线程可以共享操作系统分配给进程的内存空间,但是各个线程之间如何共享、是否完全共享需要由java虚拟机决定,粗粒度看一下JVM的内存分布:
方法区和堆是线程共享的内存区域,栈内存在线程之间不共享,那么如果多个线程同时访问方法区和堆中的数据就会出现数据不一致的问题,也就是线程安全问题;
把方法区、堆、虚拟机栈的内存看做主内存,CPU在执行线程中的命令时需要将主内存,先进行load操作,将数据从主内存加载到CPU的高速缓存(寄存器),使用完后进行save操作,将数据保存到主内存,load和save操作会造成线程共享的方法区和堆数据不一致问题及线程安全问题;
方法区存放的是类的描述信息,堆存放的是类的实例信息,直白一点就是多个线程同时访问同一个对象的类属性和实例属性,会有线程安全问题;
2.1.3 线程安全场景
Tomcat容器,容器中有一个线程池,用来处理不同的请求,但是容器中的Servlet默认是单实例的,也就是servlet的类变量和实例变量,是线程不安全的;
Spring容器,bean的scope默认时singleton,也就是单例的,如果多个线程对spring容器的bean进行访问时会有线程安全问题;
2.2 如何保证线程安全
访问共享资源的代码块叫做临界区,如果能够保证同一时刻只有一个线程执行临界区内的代码就能保证同一时刻只有一个线程在访问共享资源;
对临界区进行加锁,线程获取到锁时才可以执行临界区的代码,这样就可以通过控制锁的获取和释放来保证同一时刻只有一个线程执行临界区的代码;
锁有2种具体形式:synchronized(同步)和Lock
说明:synchronized也是锁,好比手机指纹、人脸一样,实质上都是锁只是叫法不一样
2.2.1 不添加线程安全控制
>多线程启动类
1 /** 2 * 模拟线程池,启动多个线程驱动任务执行 3 */ 4 public class ThreadSecuritySynchronized { 5 public static void main(String[] args) throws Exception { 6 int cycleCount = 10000; 7 //创建线程共享的内存资源 8 SharedResource sharedResource = new SharedResource(); 9 //开启2个线程分别增加和减少共享资源的计数器 10 //lambda表达式:Runnable是一个函数式接口,作为入参时可以用lambda表达式 11 Thread de = new Thread(() -> { 12 for (int i = 0; i < cycleCount; i++) { 13 sharedResource.decrease(); 14 } 15 }); 16 Thread in = new Thread(() -> { 17 for (int i = 0; i < cycleCount; i++) { 18 sharedResource.increase(); 19 } 20 }); 21 //线程联合,等de和in线程都执行完成后再继续执行 22 de.join(); 23 in.join(); 24 System.out.println(sharedResource.getCount()); 25 } 26 }
>共享资源类
1 /** 2 * 共享的资源类 3 * 2个线程分别增加和减少相同次数 4 * 如果最终结果为0则表示增加和减少操作是线程安全的 5 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 6 */ 7 public class SharedResource { 8 //共享资源的内存空间 9 private static int count; 10 //获取内存中的数据 11 public static int getCount() { 12 return count; 13 } 14 //修改 15 public static void increase() 16 { 17 SharedResource.count++; 18 } 19 //修改 20 public void decrease() 21 { 22 SharedResource.count--; 23 } 24 }
>运行结果:运行多次结果都不为0,说明SharedResource的更新操作不是线程安全的
2.2.2 synchronized同步
每个对象都有一把对象锁和一把类锁,同一把锁只能被一个线程占有(但是一个线程可同时占有多把锁),线程要执行synchronized修饰的代码必须取到相应的锁,否则线程就进入同步阻塞;
synchronized可以修饰方法和代码块,修饰方法时对应的锁就是方法宿主对象的对象锁和类锁,修饰代码块时对应的锁是synchronized(object)参数对象的对象锁和类锁;
synchronized修饰实例方法及实例方法内的代码块时对应的是对象锁
synchronized修饰类方法及类方法内的代码块时对应的是类锁
1. 分别给类方法和实例方法添加synchronized关键字
>共享资源类
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改:添加synchronized 17 public synchronized static void increase() 18 { 19 SharedResource.count++; 20 } 21 //修改:添加synchronized 22 public synchronized void decrease() 23 { 24 SharedResource.count--; 25 } 26 }
>运行结果:运行多次结果都不为0,说明SharedResource的更新操作仍然不是线程安全的,因为synchronized分别修饰实例方法和类方法使用的是不同的锁
2. 用synchronized关键字修饰,且都为实例方法(都为静态方法一样)
>共享资源类
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改:添加synchronized,方法改为实例方法 17 public synchronized void increase() 18 { 19 SharedResource.count++; 20 } 21 //修改:添加synchronized 22 public synchronized void decrease() 23 { 24 SharedResource.count--; 25 } 26 }
>运行结果:运行多次结果都为0,SharedResource的更新操作是线程安全的
2. 都为静态方法且synchronized修饰静态方法内的代码块
>共享资源类
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改 17 public static void increase() 18 { 19 //synchronized修饰静态代码块(SharedResource.class可以改为其他类,只要和其他synchronized保持一致即可) 20 synchronized (SharedResource.class) 21 { 22 SharedResource.count++; 23 } 24 } 25 //修改 26 public static void decrease() 27 { 28 //synchronized修饰静态代码块(SharedResource.class可以改为其他类,只要和其他synchronized保持一致即可) 29 synchronized (SharedResource.class) 30 { 31 SharedResource.count--; 32 } 33 } 34 }
>运行结果:多次运行结果都为0
2. 都为实例方法且synchronized修饰实例方法内的代码块
>共享资源类
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改 17 public void increase() 18 { 19 //synchronized修饰实例代码块(this可以改为其他对象,只要和其他synchronized保持一致即可) 20 synchronized (this) 21 { 22 SharedResource.count++; 23 } 24 } 25 //修改 26 public void decrease() 27 { 28 //synchronized修饰实例代码块(this可以改为其他对象,只要和其他synchronized保持一致即可) 29 synchronized (this) 30 { 31 SharedResource.count--; 32 } 33 } 34 }
>运行结果:多次运行结果都为0
2.2.3 Lock锁
Lock作为Java提供的一个接口,功能类似synchronized同步,但是功能比synchronized强大,可以精细化控制读写、可以响应中断、可以实现线程公平获取锁等;
1. Lock接口的6个方法
Lock作为接口提供了公共抽象方法,方法功能分为获取锁、释放锁、新建锁条件3类,其中获取锁分阻塞式获取和非阻塞式获取,阻塞式获取分为能够响应中断式和不能够响应中断式;
1. lock(): 获取锁、阻塞式、不响应中断
2. tryLock(): 获取锁、非阻塞式
3. tryLock(time, timeUnit) throws InterruptedException:获取锁、定时阻塞、响应中断
4. lockInterruptibly() throws InterruptedException:获取锁、阻塞式、响应中断
5. unLock():释放锁
6. newCondition():新建锁条件,Condition有await和signal方法,功能类似Object的wait和notify方法,常用于线程协作
2. 锁分类
根据Lock实现类的特性,锁可以分为可重入锁、公平锁、中断锁、读写锁;
1. 可重入锁:同一把锁可以锁多个临界区代码块,线程获取到锁后,可以执行同一把锁锁定的所有临界区; synchronized和Lock都是可重入锁;
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 public class ThreadSecurityLock { 4 private static Lock lock = new ReentrantLock(); 5 public static void main(String[] args ) throws Exception 6 { 7 firstGate(); 8 } 9 //synchronized和lock都是可重入锁 10 public synchronized static void firstGate(){ 11 lock.lock(); 12 try{ 13 System.out.println("into first gate."); 14 secondGate(); 15 System.out.println("out first gate."); 16 }finally { 17 lock.unlock(); 18 } 19 } 20 //在firstGate中获取到锁,可以直接进入secondGate,因为是相同的锁 21 public synchronized static void secondGate(){ 22 lock.lock(); 23 try{ 24 System.out.println("into second gate."); 25 System.out.println("out second gate."); 26 }finally { 27 lock.unlock(); 28 } 29 } 30 }
2. 公平锁:多个线程因获取锁进入阻塞状态时,阻塞时间最长的线程优先获取到锁叫做公平锁,所有阻塞线程均等机会获取到锁叫做非公平锁; synchronized是非公平锁,Lock默认是非公平锁,可以通过设置参数改为公平锁;
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 public class ThreadSecurityLock { 5 //构造函数入参为true,指定锁为公平锁 6 private static Lock lock = new ReentrantLock(true); 7 public static void main(String[] args ) throws Exception 8 { 9 new Thread(()->{ 10 lock.lock(); 11 try{ 12 //休眠10秒,让其他线程进入阻塞 13 TimeUnit.SECONDS.sleep(5); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } finally { 17 lock.unlock(); 18 } 19 }).start(); 20 //休眠是为了线程有序进入阻塞 21 TimeUnit.MILLISECONDS.sleep(1); 22 new Thread(()->{ 23 lock.lock(); 24 try{ 25 System.out.println("第一个获取锁的阻塞线程"); 26 }finally { 27 lock.unlock(); 28 } 29 }).start(); 30 //休眠是为了线程有序进入阻塞 31 TimeUnit.MILLISECONDS.sleep(1); 32 new Thread(()->{ 33 lock.lock(); 34 try{ 35 System.out.println("第二个获取锁的阻塞线程"); 36 }finally { 37 lock.unlock(); 38 } 39 }).start(); 40 //休眠是为了线程有序进入阻塞 41 TimeUnit.MILLISECONDS.sleep(1); 42 new Thread(()->{ 43 lock.lock(); 44 try{ 45 System.out.println("第三个获取锁的阻塞线程"); 46 }finally { 47 lock.unlock(); 48 } 49 }).start(); 50 } 51 }
3. 中断锁:线程在因获取锁而进入阻塞状态时,中断锁能够响应中断退出阻塞,非中断锁进入阻塞后除非获取到锁否则无法退出阻塞状态;synchronized是非中断锁,Lock不同的获取锁方法可以响应中断也可以不响应中断
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 public class ThreadSecurityLock { 5 private static Lock lock = new ReentrantLock(); 6 public static void main(String[] args ) throws Exception { 7 new Thread(() -> { 8 lock.lock(); 9 try { 10 //休眠10秒,让其他线程进入阻塞,然后可以响应中断 11 TimeUnit.SECONDS.sleep(5); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } finally { 15 lock.unlock(); 16 } 17 }).start(); 18 //休眠是为了线程有序进入阻塞 19 TimeUnit.MILLISECONDS.sleep(1); 20 Thread thread = new Thread(() -> { 21 try { 22 lock.lockInterruptibly(); 23 try { 24 System.out.println("第一个获取锁的阻塞线程"); 25 } finally { 26 lock.unlock(); 27 } 28 //获取锁,执行后退出 29 return; 30 } catch (InterruptedException e) { 31 System.out.println("中断了..."); 32 } 33 System.out.println("中断后执行..."); 34 }); 35 thread.start(); 36 //休眠是为了线程有序进入阻塞 37 TimeUnit.MILLISECONDS.sleep(1); 38 //中断线程 39 thread.interrupt(); 40 } 41 }
4. 读写锁:线程安全问题是因为不同的线程同时更新共享资源,如果所有线程只是读取共享资源的数据不会有线程安全问题,读写锁可以多个线程同时读,但不能同时读写;(A线程获取读锁时其他线程可以同时读,A线程获取写锁时其他线程只能等待)
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.ReadWriteLock; Java多线程与并发库高级应用-工具类介绍