Java线程学习详解
线程基础
1. 线程的生命周期
1.1 新建状态:
- 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
1.2 就绪状态:
- 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
1.3 运行状态:
- 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
1.4 阻塞状态:
- 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
1.5 死亡状态:
- 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
2. 线程的优先级和守护线程
2.1 线程的优先级
- 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
- Java 线程的优先级是一个整数,其取值范围是
1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )
。 - 默认情况下,每一个线程都会分配一个优先级
NORM_PRIORITY(5)
。
2.2 守护线程
- Java中有两种线程:用户线程和守护线程。可以通过isDeamon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
- 用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。
- 需要注意的是:JVM在“用户线程”都结束后会退出。
3. 创建线程
3.1 通过实现 Runnable 接口
- 步骤:
- 创建类实现 Runnable 接口
- 实现 run() 方法,线程实际运行的方法
- 实现 start() 方法,里面实例化线程对象(new Thread(this, threadName)),调用线程对象的 start() 方法
-
代码实现
“`java
package com.ljw.thread;public class RunnableDemo {
public static void main(String[] args) { // 测试 RunnableDemo R = new RunnableDemo(); RunnableThread R1 = R.new RunnableThread("thread1"); R1.start(); RunnableThread R2 = R.new RunnableThread("thread2"); R2.start(); } class RunnableThread implements Runnable{ private String threadName; private Thread t; public RunnableThread(String name) { // TODO Auto-generated constructor stub threadName = name; System.out.println("创建线程 "+threadName); } @Override public void run() { System.out.println("正在运行线程:"+threadName); try { for(int i=10;i>0;i--) { System.out.println("线程:"+threadName+" 正在打印:"+i); Thread.sleep(50); } }catch(Exception e) { e.printStackTrace(); } System.out.println("线程:"+threadName+" 正在退出......"); } public void start() { System.out.println("开始线程 "+threadName); if(t == null) { t = new Thread(this, threadName); t.start(); } } }
}
“`
3.2 通过继承 Thread 类本身
- 步骤:
- 创建类继承 Thread 类
- 下面与用Runnable接口一样
3.3 通过 Callable 和 Future 创建线程
- 步骤:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
- Callable接口与Runnable接口的区别:
- Callable中call方法可以有返回值,而Runnable中的run方法没有返回值
-
代码实现
“`java
package com.ljw.thread;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;public class CallableThreadTest implements Callable {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask ft = new FutureTask<>(ctt);
for(int i = 0;i < 10;i++)
{
System.out.println(Thread.currentThread().getName()+” 的循环变量i的值”+i);
if(i%2==0)
{
new Thread(ft,”有返回值的线程”).start();
}
}
try
{
System.out.println(“子线程的返回值:”+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (Exception e)
{
e.printStackTrace();
}} @Override public Integer call() throws Exception { int i = 0; for(;i<10;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; }
}
“`
4. synchronized关键字
4.1 概述
- synchronized关键字是为了解决共享资源竞争的问题,共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。
- 要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问的这个资源的方法标记为synchronized。
- 如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。
4.2 基本原则
- 第一条:当一个线程访问某对象的
synchronized方法
或者synchronized代码块
时,其他线程对该对象的该synchronized方法
或者synchronized代码块
的访问将被阻塞。 - 第二条:当一个线程访问某对象的
synchronized方法
或者synchronized代码块
时,其他线程仍然可以访问该对象的非同步代码块。 - 第三条:当一个线程访问某对象的
synchronized方法
或者synchronized代码块
时,其他线程对该对象的其他的synchronized方法
或者synchronized代码块
的访问将被阻塞。
4.3 实例
- 两个相似的例子
- 实例1:实现接口Runnable
package com.ljw.thread; public class RunnableTest { public static void main(String[] args) { class MyRunnable implements Runnable{ @Override public void run() { synchronized (this) { for(int i=0;i<5;i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在进行打印 " +i); } } } } Runnable runnable = new MyRunnable(); Thread t1 = new Thread(runnable,"t1"); Thread t2 = new Thread(runnable,"t2"); t1.start(); t2.start(); } }
运行结果:
t1 正在进行打印 0 t1 正在进行打印 1 t1 正在进行打印 2 t1 正在进行打印 3 t1 正在进行打印 4 t2 正在进行打印 0 t2 正在进行打印 1 t2 正在进行打印 2 t2 正在进行打印 3 t2 正在进行打印 4
结果说明:run()方法中存在synchronized(this)代码块,而且t1和t2都是基于MyRunnable这个Runnable对象创建的线程。这就意味着,我们可以将synchronized(this)中的this看做是MyRunnable这个Runnable对象;因此,线程t1和t2共享“MyRunable对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待正在运行的线程释放MyRunnable的同步锁之后才能运行。- 实例2:继承Thread类
public class ThreadTest { public static void main(String[] args) { class MyThread extends Thread{ public MyThread(String name){ super(name); } @Override public void run() { synchronized(this){ for(int i=0;i<10;i++){ try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" 正在进行打印 "+i); } catch (InterruptedException e) { e.printStackTrace(); } } } } } Thread t1 = new MyThread("t1"); Thread t2 = new MyThread("t2"); t1.start(); t2.start(); } }
运行结果:
t2 正在进行打印 0 t1 正在进行打印 0 t2 正在进行打印 1 t1 正在进行打印 1 t1 正在进行打印 2 t2 正在进行打印 2 t2 正在进行打印 3 t1 正在进行打印 3 t1 正在进行打印 4 t2 正在进行打印 4
对比结果:发现实例1的两个线程是一个结束后,另一个才运行,实例2的是交叉运行,在run()方法中都有synchronized(this),为什么结果不一样?分析:synchronized(this)中的this是指当前对象,即synchronized(this)所在类对应的当前对象。它的作用是获取获取当前对象的同步锁。对于实例2中的synchronized(this)中的this代表的是MyThread对象,t1和t2是两个不同的MyThread对象,因此t1和t2在执行synchronized(this)时获取的是不同对象的同步锁。对于实例1来说,synchronized(this)中的this代表的时候MyRunnable对象,t1和t2是共同一个MyRunnable对象,因此,一个线程获取了对象的同步锁,会造成另一个线程的等待。
4.4 synchronized方法和synchronized代码块
4.4.1 概述
-
synchronized方法
是用synchronized修饰方法,这是一种粗粒度锁;这个同步方法(非static方法)无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。 -
synchronized代码块
是用synchronized修饰代码块,这是一种细粒度锁。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,任何时候只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。虽然Java允许使用任何对象作为同步监视器,但同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
4.4.2 实例
public class SnchronizedTest {
public static void main(String[] args) {
class Demo {
// synchronized方法
public synchronized void synMethod() {
for(int i=0; i<1000000; i++)
;
}
public void synBlock() {
// synchronized代码块
synchronized( this ) {
for(int i=0; i<1000000; i++)
;
}
}
}
}
}
4.5 实例锁和全局锁
4.5.1 概述
- 实例锁:锁在某个实例对象上。如果该类是单例,那么该锁也是具有全局锁的概念。实例锁对应的就是synchronized关键字。
- 全局锁:该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
4.5.2 实例
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
假设,类Something有两个实例(对象)分别为x和y。分析下面4组表达式获取锁的情况。
- x.isSyncA()与x.isSyncB()
- 不能同时访问,因为都是访问对象x的同步锁
- x.isSyncA()与y.isSyncA()
- 可以同时访问,因为是访问不同对象(x和y)的同步锁
- x.cSyncA()与y.cSyncB()
- 不能同时访问,因为两个方法是静态的,相当于用Something.cSyncA()和Something.cSyncB()访问,是相同的对象
- x.isSyncA()与Something.cSyncA()
- 可以同时访问,因为访问不同对象
5. Volatile 关键字
5.1 Volatile原理
- Java语言提供了一种稍微同步机制,即volatile变量,用来确保将变量的更新操作通知其他线程
- 在访问volatile变量是不会执行加锁操作,因此也就不会重新执行线程阻塞,volatile变量是一种比synchronized关键字轻量级的同步机制
- 当一个变量被volatile修饰后,不但具有可见性,而且还禁止指令重排。volatile的读性能消耗与普通变量几乎相同,但是写操作就慢一些,因为它要保证本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行
6. 线程等待和唤醒
6.1 常用方法
- 在Object.java中,定义了wait(),notify()和notifyAll()等接口
- wait()方法的作用是让当前线程进入阻塞状态,同时会释放当前对象所持有的锁
- notify()唤醒当前对象上的等待线程,notifyAll()则是唤醒所有的线程
6.2 实例
package com.ljw.thread;
public class WaitDemo {
public static void main(String[] args) {
class ThreadTest extends Thread{
@Override
public void run() {
synchronized (this) {
System.out.println("开始运行线程 "+Thread.currentThread().getName());
System.out.println("唤醒线程notify()");
notify();
}
}
}
ThreadTest thread1 = new ThreadTest();
thread1.start();
synchronized (thread1) {
try {
System.out.println("主线程进入阻塞,释放thread对象的同步锁,wait()");
thread1.wait(); // wait()是让当前线程进入阻塞状态,wait()是在主线程中执行,
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程继续进行");
}
}
7. 线程让步和休眠
7.1 线程让步
7.1.1 概述
- 在Java线程中,yield()方法的作用是让步,它能让当前线程由“运行状态”进入到“就绪状态”,可能让其它同级别的线程获得执行权,但不一定,可能它自己再次由“就绪状态”进入到“运行状态”
7.1.2 实例
package com.ljw.thread;
public class YieldTest {
public static void main(String[] args) {
class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
@Override
public synchronized void run() {
for(int i=0;i<5;i++){
System.out.println(" "+this.getName()+" "+i);
if(i%2 == 0){
Thread.yield();
}
}
}
}
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
}
}
运行结果(不唯一):
t1 0
t2 0
t1 1
t1 2
t2 1
t1 3
t2 2
t1 4
t2 3
t2 4
结果说明:
线程t1在能被2整除的时候,并不一定切换到线程2。这表明,yield()方法虽然可以让线程由“运行状态”进入到“就绪状态”;但是,它不一定会让其他线程获取CPU执行权(其他线程进入到“运行状态”)。即时这个“其他线程”与当前调用yield()的线程具有相同的优先级。
7.1.3 yield()和wait()比较
- wait()的作用是让当前线程由“运行状态”进入“阻塞状态”,而yield()是让当前线程由“运行状态”进入“就绪状态”
- wait()是会让线程释放它所持有的对象的同步锁,而yield()方法不会释放对象的同步锁。
7.2 线程休眠
7.2.1 概述
- sleep()方法定义在Thread类中,sleep()的作用是让当前线程休眠,即当前线程会从“远程状态”进入到“休眠(阻塞)状态”
- sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间
- 在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待CPU的调度执行。
7.2.2 sleep() 和 wait()的比较
- wait()的作用是让当前的线程由“运行状态”进入到“等待(阻塞)状态”的同时,也会释放同步锁- 但是sleep()的作用是让当前线程由“运行状态”进入到“休眠(阻塞)”状态,但不会释放锁。
8. 加入一个线程
8.1 概述
- 在一个线程T上调用另一个线程t的 join() 方法,相当于在T中加入线程t,要等t结束后(即t.isAlive为假), join() 后面的代码块才会执行。
- 可以在调用jion()时带上一个超时参数(单位可以是毫秒,或者纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回
9. 终止一个线程
9.1 概述
- interrupt()并不会终止处于“运行状态”的线程,它会将线程的中断标记设为true。
- 综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:
@Override
public void run() {
try {
// 1. isInterrupted()保证,只要中断标记为true就终止线程。
while (!isInterrupted()) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
}
}
9.2 实例
public class InterruptBlock {
/**
* @param args
*/
public static void main(String[] args) {
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
try {
int i=0;
while(!isInterrupted()){
Thread.sleep(100);
i++;
System.out.println(Thread.currentThread().getName()+ " ("+this.getState()+") loop "+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+ " ("+this.getState()+") catch InterruptedExecption");
}
}
}
try {
//新建
Thread t1 = new MyThread("t1");
System.out.println(t1.getName()+" ("+t1.getState()+" ) is new.");
System.out.println("luo1:"+t1.isInterrupted());
//启动
t1.start();
System.out.println(t1.getName()+" ("+t1.getState()+" ) is started.");
System.out.println("luo2:"+t1.isInterrupted());
//主线程休眠300ms,然后主线程给t1发“中断”指令
Thread.sleep(300);
t1.interrupt();
System.out.println("luo3:"+t1.isInterrupted());
System.out.println(t1.getName()+" ("+t1.getState()+" ) is interrupted.");
//主线程休眠300ms,然后查看t1的状态
Thread.sleep(300);
System.out.println(t1.getName()+" ("+t1.getState()+" ) is interrupted now .");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
t1 (NEW ) is new.
luo1:false
t1 (RUNNABLE ) is started.
luo2:false
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
luo3:true
t1 (RUNNABLE) loop 3
t1 (RUNNABLE ) is interrupted.
t1 (TERMINATED ) is interrupted now .
9.3 interrupt()和isInterrupted()的区别
- interrupt()和isInterrupted()都能够用于检测对象的“中断标记”。区别是:interrupt()除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false);而isInterrupted()仅仅返回中断标记。
线程进阶
1. 线程池
- 示例
package com.ljw.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) { // 主线程
// 线程池 ---> Executors工具类(工厂类)
/*
* newFixedThreadPool(int threadCount) 创建固定数量的线程池
* newCachedThreadPool() 创建动态数量的线程池
*/
ExecutorService es = Executors.newFixedThreadPool(3);
Runnable task = new MyTask();
// 提交任务
es.submit(task);
es.submit(task);
es.shutdown(); // 关闭线程池,则表示不在接收新任务,不代表正在线程池的任务会停掉
}
}
class MyTask implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" MyTask "+i);
}
}
}
2. 线程安全与锁
2.1 重入锁和读写锁
package com.ljw.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
* ReentrantLock类,重入锁:Lock接口的实现类,与synchronized一样具有互斥锁功能 lock() 和 unlock()
* ReentrantReadWriteLock类,读写锁:一种支持一写多读的同步锁,读写分离,分别分配读锁和写锁,在读操作远远高于写操作的环境中可以提高效率
* 互斥规则:
* 写--写:互斥,阻塞
* 读--写:互斥,阻塞
* 读--读:不互斥,不阻塞
*
*/
public class LockDemo {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(20);
Student s = new Student();
// ReentrantLock rLock = new ReentrantLock(); // 用ReenTrantLock加锁运行时间20008ms
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 用读写锁分别对读写任务加锁运行时间3003ms
ReadLock rl = rwLock.readLock();
WriteLock wl = rwLock.writeLock();
// 写任务
Callable<Object> writeTask = new Callable<Object>() {
@Override
public Object call() throws Exception {
// rLock.lock();
wl.lock();
try {
Thread.sleep(1000);
s.setValue(100);
}finally {
// rLock.unlock();
wl.unlock();
}
return null;
}};
// 读任务
Callable<Object> readTask = new Callable<Object>() {
@Override
public Object call() throws Exception {
// rLock.lock();
rl.lock();
try {
Thread.sleep(1000);
s.getValue();
}finally {
// rLock.unlock();
rl.unlock();
}
return null;
}};
// 开始时间
long start = System.currentTimeMillis();
for(int i=0;i<2;i++) { // 写任务执行 2 次
es.submit(writeTask);
}
for(int i=0;i<18;i++) { // 读任务执行 18 次
es.submit(readTask);
}
es.shutdown(); // 停止线程池,不在接受新的任务,将现有任务全部执行完毕
while(true) {
if(es.isTerminated()) { // 当线程池中所有任务执行完毕,返回true,否则返回false
break;
}
}
// 执行到这里,说明线程池中所有任务都执行完毕,可以计算结束时间
System.out.println(System.currentTimeMillis()-start);
}
}
class Student {
private int value;
//读
public int getValue() {
return value;
}
//写
public void setValue(int value) {
this.value = value;
}
}
2.2 线程安全
2.2.1 Collections工具类
- Collections工具类中提供了多个可以获得线程安全集合的方法
static <T> Collection<T> synchronizedCollection(Collection<T> c)
//返回由指定集合支持的同步(线程安全)集合。
static <T> List<T> synchronizedList(List<T> list)
//返回由指定列表支持的同步(线程安全)列表。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
//返回由指定地图支持的同步(线程安全)映射。
static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m)
//返回由指定的可导航地图支持的同步(线程安全)可导航地图。
static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s)
//返回由指定的可导航集支持的同步(线程安全)可导航集。
static <T> Set<T> synchronizedSet(Set<T> s)
//返回由指定集合支持的同步(线程安全)集。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
//返回由指定的排序映射支持的同步(线程安全)排序映射。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
//返回由指定的排序集支持的同步(线程安全)排序集。
2.2.2 CopyOnWriteArrayList
- 线程安全的ArrayList
- 读写分离,写加锁,读没锁,读写之间不互斥
- 使用方法与ArrayList无异
2.2.3 CopyOnWriteArraySet
- 基于 CopyOnWriteArrayList
2.2.4 ConcurrentHashMap
- 初始容量默认为16段(Segment),采用分段锁设计
- 不对整个Map加锁,只对每个Segment加锁
- 当多个对象访问同个Segment才会互斥