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
指定长度的栈,栈如果满了,添加进栈的操作也会被阻塞,与阻塞队列不同的是,栈是先进后出的,队列是先进先出的
-