一、线程相关概念
(简单介绍一些概念,可跳过这一章,直接进入主题)
1. 进程
每个进程都有独立的代码和数据空间(进程上下文,也叫对内存),一个进程包含1到N个线程。堆内存在操作系统对进程进行初始化的时候分配,运行过程中也可向系统申请额外的内存。
2. 线程
线程是程序运行的基本单位,每个线程都有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能独立执行,必须依存在进程中。
3. 线程栈
线程栈是线程独有的,保存其运行状态和局部变量,线程栈在线程开始的时候初始化,每个线程的栈互相独立。
4. 并发与并行
并发是指通过cpu调度算法,在用户看来是同时执行的,但实际上从cpu调度层面不是真正的同时。
并行是指操作系统(也可以说多核cpu)同时执行多个程序,是真正的同时,也就是同一时间。
5. 同步与异步
Java中的同步指的是通过人为的控制和调度,保证对于共享资源的多线程访问是线程安全的,即保证在不关注线程执行顺序的情况下结果是准确的。是线程安全的。通常在代码中使用synchronized或lock来实现同步。
异步指的是多个线程访问共享资源无人为的控制和调度,在线程执行顺序不同的情况下,有可能出现数据结果不同的情况,是线程不安全的。
6. 监视器
Java会为每个Object对象分配一个monitor,当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。当线程退出同步方法时,该线程会释放monitor,这将允许其他等待的线程获得monitor以使对同步方法的调用执行下去。当以一个对象的monitor被占用时,其他线程在无法强制获取monitor的情况下,将无法调用该对象的同步方法,但可以调用非同步方法。
7. 守护线程
指为其他线程提供服务的线程。程序在所有线程(除守护线程)执行结束后退出,守护线程随程序结束而结束。如JVM的垃圾回收线程就是一个守护线程。
8. 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生线程死锁的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。 - 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
二、创建一个线程
本章的所有程序最好都自己写一遍,为加深理解,建议查看java.lang.Thread源码
1. 继承java.lang.Thread类
可通过直接继承java.lang.Thread类并重写run()方法实现具体线程业务的方式定义一个线程,创建子类的对象即可初始化一个线程,调用start()方法即可让线程进入可执行状态。
public class MyThread extends Thread{
public void run(){
for(int i = 0; i < 5; i++){
//Thread.currentThread().getName()用于获取当前线程名称
System.out.println(Thread.currentThread().getName()+"i");
}
}
public static void main(String[] args){
Thread myTh1 = new MyThread();
Thread myTh2 = new MyThread();
myTh1.start();
myTh2.start();
}
}
运行结果
java.lang.Thread类作为线程类能通过创建对象初始化一个线程,但是线程的具体业务定义在重写的run()方法里(Thread类实现了Runnable接口),通过继承Thread类并重写run()方法,子类初始化得到的线程执行时调用的是子类重写的run()方法,以此实现了创建包含具体业务的线程。
2. 实现java.lang.Runnable接口
可通过实现java.lang.Runnable接口并重写run()方法,将该子类的对象作为参数传递给Thread对象的方式实现线程初始化。调用Thread对象的start()方法即可让线程进入可执行状态。
public class MyRunnable implements Runnable{
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
Thread myTh1 = new Thread(new MyRunnable());
Thread myTh2 = new Thread(new MyRunnable());
myTh1.start();
myTh2.start();
}
}
运行结果
方法是通过实现java.lang.Runnable接口并重写run()方法来定义一个具体的线程业务。创建java.lang.Thread的对象即可初始化一个线程,将重写Runnable接口的子类对象作为参数传递给线程,线程在执行时会调用该对象的run()方法来实现具体的业务。
3. 实现Callable接口
通过实现java.util.concurrent.Callable接口并重写call()方法实现线程业务,需要将Callable子类的对象传入FutureTask构造函数,再将FutureTask对象传入Thread的构造函数实现一个线程的初始化。线程正常执行时会调用FutureTask的run()方法,run()方法会调用Callable子类重写的call()方法,并将返回值存储下来,调用FutureTask对象的get()方法即可获取该返回值。
public class MyCallable implements Callable<Object>{
@Override
public String call() throws Exception {
System.out.println("执行了call方法体");
return Thread.currentThread().getName()+"执行了callable的call";
}
public static void main(String[] args){
Callable<Object> myCallable = new MyCallable();
FutureTask<Object> futureTask = new FutureTask<Object>(myCallable);
Thread t1 = new Thread(futureTask);
t1.setName("callableThread");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
运行结果
4. 创建方法对比
实现java.util.concurrent.Callable接口的方式创建线程可以进行异常处理及获取返回值。
实际java.lang.Thread才是线程类,要启动一个线程只能通过调用Thread的start()方法的形式实现,继承Thread方法的子类也是通过调用继承自Thread的start()方法来启动线程的。故第一种方法是通过继承Thread类并重写run()方法来实现具体业务的方式来定义一个包含具体业务的线程类。
Thread类实现了Runnable接口,并且通过调用重写自Runnable接口的run()方法来实现线程业务,但Thread类的run()方法是调用的内嵌的Runnable实现类的对象target的run()方法来实现具体业务。因此第二种方式实质上是将线程的具体业务通过Runnable的实现类来定义的,通过将该实现类的对象传递给Thread的对象这种方式来初始化一个包含具体业务的线程。该种方式将具体业务与线程定义分离,提高了代码的健壮性。推荐使用第二种方法。
new Thread().start();
(这种方式其实也是启用了一个线程,只是该线程不包含任何业务,因为start()方法调用了Thread类的run()方法)
三、线程的状态
1. 线程运行的状态
线程状态:初始、可执行、运行中、阻塞、终止。
初始状态(new):新创建了一个线程对象。
可执行状态(runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
运行状态(running):可执行状态的线程获取了CPU,执行程序代码。
阻塞状态(blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入可执行状态,才有机会转到运行状态,回到运行状态时从阻塞断点继续执行程序。
死亡状态(dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2. 线程阻塞的情况
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若是该同步锁被别的线程占用,则JVM会把该线程放入锁池中。当资源释放,锁池队列中线程获得资源进入运行状态直接从阻塞的断点运行程序。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
3. 线程的调度
(1) 线程优先级
JVM提供了10个线程优先级,取值范围为int型的1~10(10为最高优先级),但该取值与常见的操作系统都不能很好的映射。通常建议使用Thread的静态常量进行优先级设置。Thread类包含三个优先级公共静态常量:
public static final int MAX_PRIORITY:线程可以具有的最高优先级,取值为10。
public static final int MIN_PRIORITY:线程可以具有的最低优先级,取值为1。
public static final int NORM_PRIORITY:分配给线程的默认优先级,取值为5,主线程默认的优先级。
Thread类提供了setPriority()和getPriority()来进行线程优先级的设置与获取。线程的优先级具有继承关系,比如主线程的默认优先级为5,在主线程中创建的子线程优先级默认与主线程一致。
(2) 线程让步
Thread的类方法yield()可暂停当前正在运行的线程使之进入可执行状态等待OS再次选中。同时建议OS选中优先级相同或更高的线程,但通常该让步操作并非每次都生效,因为OS还是可能选中原线程。并且OS也并非每次都依照线程优先级进行线程调用。
public class TestPriority {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable12(),"t1");
Thread t2 = new Thread(new MyRunnable12(),"t2");
t1.setPriority(Thread.MAX_PRIORITY);//最高优先级线程
t2.setPriority(Thread.MIN_PRIORITY);//最低优先级线程
t2.start();
t1.start();
t1.wait();
}
}
class MyRunnable12 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.yield();
}
}
}
运行结果
(3) 线程休眠
Thread的类方法sleep(long millis)可阻塞当前线程millis毫秒,在阻塞期间该线程不会被OS选中也不会释放同步锁资源,将CUP资源让给其他可执行线程。当休眠时间结束后该线程将进入可执行状态等待OS选中。
(4) 线程等待与唤醒
Object的实例方法wait()必须通过被线程获取了的同步锁对象进行调用,调用后占有该同步锁资源的线程立即释放锁资源进入阻塞状态置于等待池中。Object的实例方法notify()也必须通过被线程获取了的同步锁对象进行调用,调用之后会在该锁资源对应的等待池中随机唤醒一个线程,被唤醒的线程进入锁池等待锁资源被释放后与其他线程一同竞争资源。notify()调用后并不会立即释放锁资源,需待synchronized代码块执行完后释放。
线程等待wait()与线程休眠sleep()的区别:
- wait()方法的调用会使线程失去锁资源并进入阻塞状态;sleep()调用后线程进入阻塞状态但会保持对锁资源的占有。
- wait()调用进入阻塞状态必须等待notify()唤醒,并且唤醒后需竞争到对应锁资源才能进入可执行状态等待被OS选中执行;sleep()调用进入阻塞状态休眠指定毫秒数后线程会直接进入可执行状态。
(5) 线程挂起
在主线程中如果需要用到子线程的计算结果,而子线程又涉及耗时的操作,则需要子线程对象调用join()函数,调用之后主线程立即阻塞被挂起,待子线程执行结束之后再继续执行。
四、线程同步
1. synchronized关键字实现同步
synchronized可用于修饰同步方法和代码块。Synchronied的作用是为资源上锁作为一个“互斥量”,当一个线程获得该资源时,其他线程需等资源释放后才能获取资源再继续执行,从而实现了线程同步机制。
(1)synchronized修饰方法
synchronized可用于修饰实例方法和static方法:
当修饰实例方法时,一个线程调用一个对象的实例同步方法,其他线程则无法调用该对象的任何实例同步方法,但可以调用非同步方法和staic同步方法。
当修饰static方式时,一个线程调用一个类的static同步方法,则其余线程无法调用该类的任何static同步方法,但可以调用非同步方法和实例同步方法。
(2)synchronized修饰代码块
需要明确的是,当synchronized修饰代码块时,线程是都可以调用该代码所属方法的(该方法为非同步方法),只有方法体执行至同步代码块时才需要竞争资源,获得资源的线程继续执行,其余线程进入阻塞状态,待资源释放后竞争到资源再继续执行。
public class SynchronizedTest {
class TestSync{//内部类
Byte a = 0;
public void syncA(){
System.out.println(Thread.currentThread()+"开始执行");
synchronized(a){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"执行完毕");
}
}
}
public static void main(String[] args) {
TestSync testSync =new SynchronizedTest().new TestSync();
Thread th1 = new Thread(new Runnable(){
@Override
public void run() {
testSync.syncA();
}
});
Thread th2 = new Thread(new Runnable(){
@Override
public void run() {
testSync.syncA();
}
});
th1.start();
th2.start();
}
}
运行结果
2. java.util.concurrent.locks包的接口和类实现同步
另一种实现同步锁的方式就是使用Lock相关类创建实例,通过实例调用方法来实现加锁和解锁。常用的接口和类包括:Lock、ReadWriteLock、ReentrantLock、ReentrantReadWriteLock。
(1)Lock及其实现类ReentrantLock
ReentrantLock主要实现了接口Lock的lock()、unlock()、tryLock()、tryLock(long timeout,TimeUnit unit)、lockInterruptibly()等方法。
lock():当线程执行到调用lock()方法时则进行加锁操作,若加锁成功则继续执行,若当前有其他线程占有锁则当前线程进入等待状态,直到锁被释放并获取到锁。
unlock():使用lock()方法加锁之后,线程即便执行结束也不会主动释放锁,所以需要调用unlock()方法来进行锁释放操作,否则容易造成死锁。通常在调用lock()之后会把接下来的业务都放在try{…}catch(…){…}结构中,并在finally释放锁。
tyLock():该方法用于尝试加锁操作,若加锁成功则立即返回true,若加锁失败则立即返回false,不会进入等待状态。
tryLock(long timeout,TimeUnit unit):该方法与tryLock()相似都是进行尝试加锁操作,若加锁成功则立即返回true,不同的是若加锁失败不会立即false,会等待timeout时间,在等待过程中若加锁成功则返回true,若超过等待时间则返回false。
lockInterruptibly():该方法与lock()相似,都是进行加锁操作,若加锁成功则继续执行,若加锁失败则线程进入等待状态,不同的是本方法在等待状态时线程可被别的线程中断。
在使用Lock对象进行加锁操作的时候需注意进行解锁操作,因为Lock对象加锁之后即使是线程执行完毕也不会主动释放锁,必须手动解锁。Lock的加锁操作类似于synchronized代码块,只会控制线程对lock()方法调用位置开始的代码,在调用之前的代码所有线程都可执行。另外,进行加锁解锁操作必须针对同一个Lock实例。
public class LockTest {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockTest lockTest = new LockTest();
new Thread(new Runnable(){
@Override
public void run() {
lockTest.testLock();
}}).start();
new Thread(new Runnable(){
@Override
public void run() {
lockTest.testLock();
}}).start();
}
public void testLock(){
System.out.println("调用了testLock方法");
lock.lock();
try{
Thread.sleep(1000);
System.out.println(Thread.currentThread()+"加锁成功");
Thread.sleep(2000);
}catch(Exception e){
}finally{
lock.unlock();
}
}
}
运行结果
(2)ReadWriteLock及其实现类ReentrantReadWriteLock
ReentrantReadWriteLock是Lock的另一种实现方式,它在内部维护了两个锁,分别是“读锁”和“写锁”。“读锁”用于线程的读取操作,由于读操作不会破坏数据,往往需要允许多个线程同时进行读操作,故“读锁”可被多个线程同时获取。“写锁”主要用于写操作,写操作一般比较危险,如果不进行同步控制容易导致数据异常,故需要限制“写锁”被多个线程获取,“写锁”只能被单个线程获取。
值得注意的是,ReentrantReadWriteLock在所有“读锁”未解锁之前不允许任何线程获取“写锁”,但允许同一线程在持有“写锁”的情况下获取“读锁”。
public class ReentrantReadWriteLockTest {
final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
final Lock readLock = rw.readLock();
final Lock writeLock = rw.writeLock();
int a = 0;
public int getValue(){
readLock.lock();
try{
System.out.println(Thread.currentThread()+"获取了读锁");
Thread.sleep(2000);
System.out.println(Thread.currentThread()+"获取了"+a);
}catch(Exception e){
}finally{
System.out.println(Thread.currentThread()+"释放了读锁");
readLock.unlock();
}
return a;
}
public void setValue(int b){
writeLock.lock();
try{
System.out.println(Thread.currentThread()+"获取了写锁");
a = b;
Thread.sleep(2000);
System.out.println(Thread.currentThread()+"设置了"+b);
}catch(Exception e){
}finally{
System.out.println(Thread.currentThread()+"释放了写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockTest rt = new ReentrantReadWriteLockTest();
//启动一个读线程
new Thread(new Runnable(){
@Override
public void run() {
rt.getValue();
System.out.println(Thread.currentThread()+"执行完毕");
}
}).start();
//启动一个读线程
new Thread(new Runnable(){
@Override
public void run() {
rt.getValue();
System.out.println(Thread.currentThread()+"执行完毕");
}
}).start();
//启动一个写线程
new Thread(new Runnable(){
@Override
public void run() {
rt.setValue(2);
System.out.println(Thread.currentThread()+"执行完毕");
}
}).start();
//启动一个读线程
new Thread(new Runnable(){
@Override
public void run() {
rt.getValue();
System.out.println(Thread.currentThread()+"执行完毕");
}
}).start();
}
}
运行结果
以上只是对可重入读写锁进行了简单的运用,有兴趣可以尝试验证一下:写锁无法同时被多个线程获取、同一个线程中可以先获取写锁再获取读锁等情况。
线程就先介绍到这里,有兴趣可以继续阅读下一篇关于线程池的介绍《Java线程进阶-运用线程池》
~~~~有问题欢迎留言交流~~~~