Java线程与多线程
一、Java线程相关概念
1、程序、进程、线程、多线程
- 程序:程序是指令和数据的有序集合(安装在磁盘),是 静态 的概念。
- 进程:进程是运行中/执行中的一段程序;进程是程序的一次执行过程,或是正在运行的一个程序。进程是 资源分配 中的最小单位,是 动态 的概念(有它自身的产生、存在、消亡的过程),每个进程有独立的资源空间。
- 线程:线程是由进程创建的,是进程中的一个实体;线程是程序执行(资源调度)的最小单位,一个进程中执行的每一个任务即为一个线程,一个进程可以拥有多个线程。
- 单线程:同一时刻,只允许执行一个线程。
- 多线程:同一时刻,可以执行多个线程;(多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。)
进程与线程的区别:
- 根本不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位。
- 资源方面:同一个进程下的线程共享进程中的一些资源。线程同时拥有自身的独立存储空间。进程之间的资源通常是独立的。
- 数量不同:进程一般指的就是一个进程。而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程。
- 开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通讯,相当方面。
2、串行、并行、并发
- 串行:在时间上不可能发生重叠,前一个任务没有完成,下一个任务就只能等着。
- 并行:在时间上是重叠的,两个或多个任务在同一时间互不干扰的同时进行。(多核cpu可以实现并行)
- 并发:允许两个任务彼此干扰,同一时间点,只有一个任务运行,交替执行。(即单核cpu实现的多任务就是并发)
3、多线程的特点及目的
多线程特点:
- 一个进程可以包含一个或多个线程。
- 一个程序实现多个代码同时交替运行就需要产生多个线程。
- 线程本身不拥有系统资源,与同属一个进程的其他线程共享所在进程所拥有的资源。
- 同一进程中的多个线程之间可以并发执行。CPU会随机抽出时间,让我们的程序一会儿做这件事,一会儿做另外一件事情。
多线程的目的:
- 最大限度的(充分)利用CPU资源。
- 当某一线程的处理不需要占用CPU而只和I/O等资源打交道时,让需要占用CPU资源的其他线程有机会获得CPU资源,从根本上说,这是多线程编程的最终目的。
多线程的局限性:
- 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
- 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
- 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
4、并发的三大特性
- 原子性:指的是一个或多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
- 可见性:指的是多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
- 有序性:即程序的执行顺序按照代码的先后顺序来执行。
5、同步、异步
-
同步:两个完全独立、不相关的任务,后一个线程必须等前一个线程结束才可以开始,等待的时间长,系统运行效率大幅度降低,即排队执行。(需要等待结果返回)
-
异步:在同一时间内可以执行多个任务,这也是多线程技术的优点,使用多线程就是在使用异步。(不需要等待结果返回就继续进行)
-
同步与异步:执行某个功能后,被调用者是否会主动反馈信息
-
阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果的反馈。
6、多线程技术使用场景
- 阻塞:一旦系统出现了阻塞线程,则可以根据实际情况来使用多线程提高效率。
- 依赖:业务分为两个执行过程,分别是A和B,当A业务有阻塞的情况发生时,B业务的执行不依赖A业务的执行结果,这时可以使用多线程技术。
7、线程安全与非线程安全
- 线程安全:指获得实例变量的值是经过同步处理的,不会出现脏读现象。
- 非线程安全:会在多个线程对同一个对象中的同一个实例变量进行并发访问时发生,产生的结果就是“脏读”,即读到的数据是被更改过的。
8、立即加载与延迟加载
- 立即加载:就是使用类的时候已经将对象创建完毕
- 延迟加载:就是调用get()方法时,实例才会被工厂创建,常见的即在get()方法中进行new实例化。
9、公平锁与非公平锁
- 公平锁:采用先到先得的策略,每次获取锁之前都会检查队列里有没有排队等待的线程,没有才会尝试获取锁,有就将当前线程追加到队列中。
- 非公平锁:采用“有机会插队”策略,一个线程获取锁之前要先去尝试获取锁,而不是在队列中等待,如果真的获取锁成功,说明线程虽然是后启动的。
二、创建线程方法
1、通过继承Thread类创建线程
步骤:
- 创建一个类,并继承Thread
- 重写run方法
- 将要执行的代码写在run方法中
- 创建Thread类的子类对象
- 启动线程
普通的Java类如果继承自Thread类,就成为一个线程类,并可通过该类start方法来启动线程,执行线程代码。
Thread类的子类可直接实例化,但在子类中必须覆盖run方法才能真正运行线程的代码。
实例1:
// 1. 创建一个类,并继承Thread
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
// 2. 重写run方法
public void run() {
// 3. 将要执行的代码写在run方法中
for(int i=0;i<5;i++) {
//getName()获得线程名
System.out.print(this.getName()+":"+i);
}
}
}
public class HelloThreadDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 4. 创建Thread类的子类对象
HelloThread h1=new HelloThread("A");
//h1.setName("线程1"); //setName();给线程命名
h1.start();
HelloThread h2=new HelloThread("B");
//h2.setName("线程2");
5. 启动线程
h2.start();
}
}
2、通过实现Runnable接口创建线程
Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时再用继承Thread类方法来创建线程显然不可能了。
实现Runnable接口的类必须借助Thread类才能创建线程,通过Thread类的start方法启动线程。(底层实现运用了 静态代理 设计模式)
步骤:
- 创建实现Runnable接口的类的实例
- 创建一个Thread类对象,将第一步实例化得到的Runnable对象作为参数传入Thread类的构造方法。
- 启动线程
实例:
class HelloRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<5;i++) {
//currentThread()返回当前正在执行的线程对象的引用。
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class HelloRunnableDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 1. 创建实现Runnable接口的类的实例
HelloRunnable helloRunnable=new HelloRunnable();
// 2. 创建一个Thread类对象,将第一步实例化得到的Runnable对象作为参数传入Thread类的构造方法。
Thread t1 = new Thread(helloRunnable,"A");
// 3. 启动线程
t1.start();
Thread t2 = new Thread(helloRunnable,"B");
t2.start();
}
}
- 匿名内部类方式:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("匿名内部类:" + i);
}
}
});
- lambda方式:
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("lambda:" + i);
}
});
3、通过Callable和Future创建线程
Callable一般用于有返回结果的非阻塞的执行方法(同步非阻塞)。
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
实例:
public class MiTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建MyCallable
MyCallable myCallable = new MyCallable();
//2. 创建FutureTask,传入Callable
FutureTask futureTask = new FutureTask(myCallable);
//3. 创建Thread线程
Thread t1 = new Thread(futureTask);
//4. 启动线程
t1.start();
//5. 做一些操作
//6. 要结果
Object count = futureTask.get();
System.out.println("总和为:" + count);
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
public static void main(String[] args) {
FutureTask<Integer> task3 = new FutureTask<>(() -> {
System.out.println("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待task执行完毕的结果
try {
Integer result = task3.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
4、区别
- Thread和Runnable的实质是继承关系,没有可比性。都会new Thread,然后执行run方法。如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
- Runnable接口解决不能再继承的问题,间接实现“多继承”的效果。使用Runnable接口方式实现多线程可以把“线程”和“任务”分离,Thread代表线程,Runnable代表可运行的任务。
- Runnable接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果,Callable接口接口的call方法允许抛出异常,而Runnable接口的run方法的异常只能在内部消化,不能往上抛出。
三、线程的基础
1、线程机制
一个线程类如下面实例中,当启动main方法即进行Run操作的时候就相当于启动了一个进程,随着又启动一个main的主线程,进入到主线程后,随之主线程中又启动了一个cat的子线程。当main线程启动一个子线程,主线程不会阻塞,会继续执行,即启动之后,主线程main会与子线程cat会交替执行。
实例:
public class Thread1 {
public static void main(String[] args) throws InterruptedException {
// 创建Cat对象,可以当作线程使用
Cat cat = new Cat();
cat.start(); // 启动线程最终会执行线程里的run方法
// 说明,当main线程启动一个子线程,主线程不会阻塞,会继续执行
System.out.println("主线程继续执行" + Thread.currentThread().getName());
for (int i = 0; i < 10; i ++) {
System.out.println("主线程 i=" + i);
Thread.sleep(1000);
}
}
}
class Cat extends Thread {
int times = 0;
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world"+ ++times);
if (times == 10) {
break;
}
}
}
}
由如下运行结果可看出启动进程之后,主线程main会与子线程cat会交替执行。
启动线程最终会执行线程里的run()方法,但是启动线程不是直接调用run()方法,而是调用start()方法。因为run()方法就是一个普通的方法,调用它并没有真正启动一个线程。
Cat cat = new Cat();
cat.start();
调用的start()方法会调用底层源码的start()方法,底层start()方法中会调用一个start0()方法
而start0()方法调用的又是一个 本地方法 start0(),该方法是由JVM调用的,底层是C/C++实现的。即最终是由这个本地方法stat0()用多线程的机制调用了run()方法
start()方法耗时多的原因是内部执行了多个步骤:
- 通过JVM告诉操作系统创建Thread
- 操作系统开辟内存并使用windows SDK中的CreateThread()函数创建Thread线程对象。
- 操作系统对Thread对象进行调度,以确定执行时间
- Thread在操作系统中被成功执行。
1)run方法里面的代码就是线程对象要执行的任务,是线程的入口。
2)多次调用start()方法,则出现异常Exception in thread “main” java.lang.IllegalThread-StateException。
3)代码的运行结果域代码执行顺序或调用顺序无关,线程是一个子任务,cpu是以不确定的方式或以随机的时间来调用线程中的run方法。
4)执行start()方法的顺序不代表线程启动的顺序,即不代表run方法执行的顺序,执行run方法的顺序是随机的。
5)多线程随机输出的原因是CPU将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替执行并输出,导致输出结果呈乱序。
6)时间片即CPU分配给各个程序的时间,每个线程被分配一个时间片,在当前的时间片内执行线程中的任务。当CPU在不同线程上进行切换时是需要耗时的,所以并不是创建的线程越多,软件运行效率就越快,相反,线程数过多反而会降低软件的执行效率。
7)如果调用代码thread.run();而不是thread.start()就不是异步执行了,而是同步执行,那么此线程对象并不交给线程规划器来进行处理,而是由main线程来调用run方法,也就是必须等run方法代码执行完成才可以执行后面的代码。
2、线程常用方法
- setName:设置线程名称,使之与参数name相同。
- getName:获取该线程的名称。
- start:使该线程开始执行;Java虚拟机底层调用该线程的start0方法。 start 底层会创建新的线程,调用run,run就是一个简单的方法调用,不会启动新线程。
- run: 调用线程对象run方法。
- setPriority:设置线程的优先级。线程的优先级用数字表示,范围1~10;Thread.MAX_PRIORITY:10、Thread.MIN_PRIORITY:1、Thread.NORM_PRIORITY:5
- getPriority:获取线程的优先级。
- sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂行执行)。 线程的静态方法,使得当前线程休眠。 每个对象都有一个锁,sleep不会释放锁。
- currentThread():返回代码段正在被哪个线程调用。
- isAlive():判断线程对象是否存活。
- getStackTrace():返回一个表示该线程的堆栈跟踪元素数组,如果该线程尚未启动或已经终止,则返回一个零长度的数组,如果不是零长度的,则其第一个元素代表堆栈顶,是该数组中最新的方法调用。
- dumpStack():作用是将当前线程的堆栈信息输出到标准错误流,仅用于调试。
- interrupt:中断线程。该方法仅仅是在当前线程中打了一个停止的标记, 但并没有真正的结束线程。当线程执行到一个 interrupt 方法时,就会出现一个 InterruptedException 异常,可catch捕获该 中断异常 加入业务代码。所以一般用于 中断正在休眠的线程
- public static boolean interrupted():测试currentThread()当前线程是否已经是中断状态,执行后具有清除状态的功能
- public boolean this.isInterrupted():测试this关键字所在线程类的对象是否已经中断,不具有清除状态的功能。
1)interrupted() 方法判断当前线程是否是停止状态,第二次调用返回false的原因:
测试当前线程是否已经中断,线程的中断状态由该方法清除,换句话说,如果连续两次调用该方法,则第二次调用将返回false,即在第一次调用已清除其中断状态。即interrupted()方法具有清除状态的功能(第二次调用检查中断状态前线程再次被中断的情况除外)
2)isInterrupted():不是静态的方法,具体取决于调用这个方法的线程对象。不具备清除状态标记功能。
3)interrupt():该方法仅仅是在当前线程中打了一个停止的标记,并没有真正的结束线程
为了彻底中断线程,需要保证run()方法内部只有一个try-catch语句块,不要出现多个try-catch块并列的情况。不管其调用顺序,只要interrupt()和sleep()方法碰到一起就会出现异常。
如果线程在sleep状态下调用interrupt()方法会出现异常,则该线程会进入catch语句,并且清除停止状态值、变成false。
调用interrupt()给线程打了中断的标记,再执行sleep()方法也好出现异常。
4)stop():强行停止线程,即暴力停止线程,容易造成业务处理的不确定性。
被stop()暴力停止的线程连一个类似执行finally()语句的机会都没有,就彻底杀死了。
线程是否被暴力停止由外界(main方法)决定,也可以根据判断条件自己调用stop() 方法完成对自身的暴力停止,线程自身调用stop() 方法会进入catch(ThreadDeath e)代码块,外界调用stop() 方法后线程内部也会进入catch代码块中。在外界对线程对象调用stop() 方法后,线程内部会抛出ThreadDeath异常,外界不会抛出ThreadDeath异常。
5)suspend()方法:暂停线程,意味着此线程还可以恢复运行;resume()方法即来恢复线程的执行。
缺点:如果suspend()方法和resume()方法使用不当,极易造成公共同步对象被独占,其他线程无法访问公共同步对象的结果,也容易出现因为线程暂停导致数据不完整。
6)stop() 和suspend()区别: stop()方法用于销毁线程对象,如果继续运行线程,则必须使用start()重新启动线程,而suspend()方用于让线程不再执行任务,线程对象并不销毁,只在当前执行的代码处暂停,未来还可恢复运行。
7)LockSupport.park() 方法:将线程暂停;LockSupport.unpark() 方法的作用是恢复线程的运行。如果先执行unpark()再执行park()方法,则park()方法不会呈暂停的效果。
3、线程礼让和线程插队
-
yield:线程礼让。让出cpu,让其他线程执行,让当前正在执行的线程停止,但不阻塞,将线程从运行状态转为就绪状态,但礼让的时间不确定,有可能刚放弃,马上又获得CPU时间片,所以不一定礼让成功。
-
join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务。有些类似synchronized同步的运行效果,区别在于join()在内部使用wait()方法进行等待,会释放锁;而synchronized关键字一直持有锁。
-
join()无参方法或join(time)有参方法一旦执行,说明源代码中的wait(time)已经被执行,也就证明锁被立即释放,仅仅在指定的join(time)时间后当前线程才会继续运行。
-
join(long)方法和sleep(long)区别:
join(long)方法和sleep(long)具有相似的功能,就是使当前线程暂停指定的时间。join(long)方法暂停的时间是可变的,取决于线程是否销毁;sleep(long)暂停的时间是固定的。关键区别在于join释放锁,sleep不释放锁。
线程 礼让、插队 实例:
public class Thread3 {
public static void main(String[] args) throws InterruptedException {
// 创建Cat对象,可以当作线程使用
T t1 = new T();
t1.start();
// 说明,当main线程启动一个子线程,主线程不会阻塞,会继续执行
System.out.println("主线程继续执行" + Thread.currentThread().getName());
for (int i = 1; i <= 10; i ++) {
Thread.sleep(1000);
System.out.println("主线程(小弟) 吃了 " + i + "包子");
if (i == 5) {
System.out.println("主线程(小弟) 让 子线程(老大) 先吃...");
// join:线程插队
t1.join(); // 这里相当于 让t1线程先执行完毕
// 礼让, 不一定成功
// Thread.yield();
System.out.println("子线程(老大)吃完了 主线程(小弟) 接着吃...");
}
}
}
}
class T extends Thread {
int times = 0;
public void run() {
for (int i = 1; i <= 10; i ++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程(老大) 吃了 " + i + "包子");
}
}
}
join:线程插队 输出结果:
可见,一开始主线程与子线程依次交替执行,当有子线程插队后,会让该子线程执行完所有的任务,再继续执行主线程的任务。
yield礼让 输出结果:
可见,yield礼让不一定成功,其后还是主线程与子线程交替执行,且还存在主线程先执行的情况。
4、线程分类
线程分为 守护线程 和 用户线程。
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。
- 守护线程:为所有非守护线程(即用户线程)提供服务的线程;任何一个守护线程都是整个JVM中非守护线程的保姆。守护线程类似于整个进程的一个默默无闻的服务者,生死无关重要。当所有的用户线程结束,守护线程自动结束。(常见的守护线程:垃圾回收机制、监控内存、后台记录操作日志)
- 虚拟机必须确保用户线程执行完毕。
- 虚拟机不用等待守护线程执行完毕。
将线程设置为守护线程:将线程实例调用setDaemon(true),设为true即可。
实例:
public class Thread4 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
// 如果希望当main线程结束后,子线程自动结束,只需要将子线程设置为守护线程即可
// 如若不设为守护线程,子线程会一直运行
myDaemonThread.setDaemon(true); // 设为守护线程
myDaemonThread.start();
for (int i = 1; i <= 10; i++) {
System.out.println("正在工作中。。。");
Thread.sleep(100);
}
}
}
// 守护线程
class MyDaemonThread extends Thread {
@Override
public void run() {
for (; ; ) { // 无限循环
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("哈哈哈。。。。。");
}
}
}
5、线程的生命周期
线程通常有五种状态:创建、就绪、运行、阻塞、死亡。
阻塞又分为三种情况:
- 等待阻塞(WAITING):运行的线程执行 wait 方法,该线程会释放占用的所有资源,JVM会把该线程放入 等待池 中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法。
- 同步阻塞(BLOCKED):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入 锁池 中。
- 其他阻塞(TIME_WAITING):运行的线程执行sleep或join方法,或者发生了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。
针对传统的如操作系统层面线程状态分为5种:
Java中线程状态分为6种:
线程的生命周期:
- 新建状态(new):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变的可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入到就绪状态,才能有机会转到运行状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
实例:
public class Thread5 {
public static void main(String[] args) throws InterruptedException {
ThreadState threadState = new ThreadState();
System.out.println(threadState.getName() + " 状态 " + threadState.getState());
threadState.start();
while (Thread.State.TERMINATED != threadState.getState()) {
System.out.println(threadState.getName() + " 状态 " + threadState.getState());
Thread.sleep(500);
}
System.out.println(threadState.getName() + " 状态 " + threadState.getState());
}
}
class ThreadState extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i ++) {
System.out.println("ha " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5、线程优先级
- 线程可以划分优先级,优先级较高的线程得到的CPU资源较多,即CPU优先执行优先级较高的线程对象中的任务,即让高优先级的线程获得更多的CPU时间片。
- 设置线程优先级有助于“线程规划器”确定在下一次选择哪一个线程来优先执行。
- 高优先级的线程总是大部分先执行完成,但不代表高优先级的线程全部先执行完,也不是先调用就先执行完。
- 线程的优先级具有继承性。线程的优先级具有随机性。
- 不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每次都先执行完,具有不确定性、随机性。
四、线程同步
1、线程同步机制
需要同步的原因:
- 线程同步是为了防止多个线程访问一个数据对象时,对数据造成破坏。(如一些敏感数据不允许被多个线程同时访问,此时就需要使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。)
- 线程同步是保证多线程安全访问竞争资源的一种手段。(即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。)
- 线程同步:多个线程操作同一个资源;并发:同一个对象被多个线程同时操作
- 线程访问的对象中如果有多个实例对象,则运行的结果有可能出现交叉的情况。如果对象仅有一个实例变量,则可能出现覆盖的情况。
- 多个线程对共享的资源有写操作,则必须同步,如果只是读取操作,则不需要同步。
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
售票超卖现象:上万人同时一百张票,如下代码可能出现超卖现象
public class SellTicket {
public static void main(String[] args) {
SellTicket01 sellTicket01 = new SellTicket01();
SellTicket01 sellTicket02 = new SellTicket01();
SellTicket01 sellTicket03 = new SellTicket01();
// 当三个线程同时进入run方法,可能出现超卖现象
sellTicket01.start();
sellTicket02.start();
sellTicket03.start();
sellTicket01.setPriority(Thread.NORM_PRIORITY);
SellTicket02 sellTicket021 = new SellTicket02();
// 当三个线程同时进入run方法,可能出现超卖现象
// 三个线程操作同一个对象,共享同一个变量
new Thread(sellTicket021).start();
new Thread(sellTicket021).start();
new Thread(sellTicket021).start();
}
}
// 使用Thread方式
class SellTicket01 extends Thread {
private static int ticketNum = 100; // 让多个线程共享
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
}
// 使用实现接口方式
class SellTicket02 implements Runnable {
private int ticketNum = 100; // 让多个线程共享
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
}
运行结果:
2、线程同步与互斥锁
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象
- 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
- synchronized方法控制对 “对象” 的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
- 同步的局限性:导致程序的执行效率降低
- 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身(当前类.class)
(1) 同步方法:
用synchronized修饰方法,为同步方法,可实现资源共享,解决售票超卖现象,此时锁 在 this 对象 上:
public class SellTicket {
public static void main(String[] args) {
// 解决超卖问题
SellTicket03 sellTicket031 = new SellTicket03();
// 三个线程操作同一个对象,共享同一个变量
new Thread(sellTicket031).start();
new Thread(sellTicket031).start();
new Thread(sellTicket031).start();
}
}
// 使用实现接口方式,使用synchronized实现线程同步
class SellTicket03 implements Runnable {
private int ticketNum = 100; // 让多个线程共享
private boolean loop = true; // 控制run方法退出变量
@Override
public void run() {
while (loop) {
sell();
}
}
// synchronized 同步方法,在同一时刻,只有一个线程来执行sell方法
// 锁在 this 对象 上
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
(2) 同步代码块:
以上的sell方法的synchronized也可写在代码块上,互斥锁在 this 对象上:
// synchronized 同步代码块
public void sell2() {
synchronized (this) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
synchronized 同步代码块,锁也可以在其他对象上,但需要注意的是,必须是各线程操作同一对象:
Object object = new Object();
// synchronized 同步代码块 锁 在 object 对象上,
// 各线程 操作同一个线程实例,则object是同一个对象
public void sell3() {
synchronized (object) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
以上代码各线程都是操作的sellTicket031对象,所以是操作的同一个object对象,但如下synchronized (new object())代码块中则各线程都各自实例化一个object对象,则不是同一个对象,此时仍可能会出现超卖现象:
// synchronized 同步代码块 各线程进入 会创建一个自己的object对象,则可能出现超卖现象
public void sell4() {
synchronized (new Object()) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
}
}
synchronized互斥锁 要求多个线程的锁对象为同一个,则使用 Thread方式创建的线程,用synchronized是无法锁住的,因其启动一个线程都得 new一个线程调用start(),即为不同一个线程对象:
// 使用Thread方式,不为同一个线程对象,即可能出现超卖现象
// new SellTicket01().start();
// new SellTicket01().start();
class SellTicket01 extends Thread {
public void m1() {
synchronized (this) {
System.out.println("不是同一个线程对象");
}
}
}
(3)synchronized修饰静态方法(static)
静态方法中使用synchronized,锁在当前类 本身上; 且在静态方法中,实现一个同步代码块,synchronized不能加在 this 上:
// synchronized 修饰静态方法, 锁在 当前类本身 SellTicket03.class 上
public static synchronized void print() {
System.out.println("synchronized修饰静态方法");
}
// 在静态方法中,实现一个同步代码块,synchronized不能加在 this 上
public static void print1() {
synchronized (SellTicket03.class) {
System.out.println("synchronized修饰静态方法");
}
}
-
synchronized为可重入锁,指自己可以再次获取自己的内部锁,子类完全可以通过锁重入调用父类的同步方法。
-
volatile使用特性:
1)可见性:B线程能马上看到A线程更改的数据
2)原子性:指一组操作在执行时不能被打断
3)禁止代码重排序 -
volatile是不支持运算原子性的,多个线程对用volatile修饰的变量i执行i++或i–操作时,会被分解成三步,造成非线程安全问题。
-
volatile关键字提示线程每次从公共内存中去读取变量,而不是从私有内存中去读取,这样保证了同步数据的可见性。
-
synchronized锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致则升级锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁对象从轻量级升级为重量级锁,就构成了synchronized锁升级。升级目的:为了减低锁带来的性能消耗。
3、线程死锁
线程同步产生死锁的原因:
当一个线程已经获取了对象1的锁,同时又想获得对象2的锁。而此时另一个线程当前已经持有了对象2的锁,而又想获取对象1的锁。这种相互等待对方释放锁的过程,会导致“死锁”。(多个线程都占用了对方的锁资源,但不肯相让,导致了死锁)
经典死锁实例:
public class DeadLock {
public static void main(String[] args) {
DeadLockThread A = new DeadLockThread(true);
DeadLockThread B = new DeadLockThread(false);
A.start();
B.start();
}
}
class DeadLockThread extends Thread {
static Object o1 = new Object(); // static 保证多线程,共享一个变量
static Object o2 = new Object();
boolean flag;
public DeadLockThread(boolean flag) {
this.flag = flag;
}
// 如果flag 为 true, 线程就会先得到o1的锁,然后尝试去获取o2对象锁
// 如果线程A 得不到 o2对象锁,就会Blocked
// 如果flag 为false,线程B 就会先得到o2的对象锁,然后尝试去获取o1的对象锁
// 如果线程B得不到o1对象锁,就会Blocked
@Override
public void run() {
if (flag) {
synchronized (o1) { // 对象互斥锁
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) { // 获得li对象的监视权
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) { // 获得li对象的监视权
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
代码输出结果:可见两个锁互不退让,卡住了。
4、释放锁
- 当前线程的同步方法,同步代码块执行结束 而释放锁
- 当前线程在同步代码块、同步方法中遇到break、return 而释放锁
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束,而释放锁
- 当前线程在同步代码块、同步方法中执行了线程对象的wait方法,当前线程暂停,并释放锁。
注意:
- 线程执行同步代码块或同步方法时,程序调用Threa.sleep()、Thread.yield()方法暂停当前线程的执行 不会 释放锁。
-线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。(尽量避免使用)
5、Lock(锁)
-
从jdk5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。
-
Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
-
ReentranLock类(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
-
加锁:lock.lock() ; 解锁: lock.unlock();
-
ReentranLock默认情况下使用的是非公平锁,如果要使用公平锁,只需要传参为true即可。
-
ReentranLock的tryLock()方法:作用是嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,那么程序继续执行后面的代码,而不是呈阻塞等待锁的状态。
-
ReentranLock类具有完全互斥排他的特点,同一时间只有一个线程在执行lock()方法后面的任务。
-
ReentranReadWriteLock:可以在同时进行读操作时不需要同步执行,提升运行速度,加快运行效率,俩个类之间没有继承关系。读写锁表示有两个锁,一个是读操作相关的锁,也叫共享锁;另一个是写操作相关的锁,也叫排它锁。读锁之间不互斥,读锁和写锁互斥,写锁和写锁互斥。**说明只要出现写锁,就会出现互斥同步的效果。**读操作是指读取实例变量的值;写操作是指向实例变量写入值。
-
ReentranLock类和ReentranReadWriteLock类相比主要都缺点是使用ReentranLock对象时所有的操作都同步,哪怕只对实例变量进行读取操作也会同步处理,耗费大量时间,降低运行效率。
实例代码:
public void sell4() {
try {
lock.lock(); // 加锁
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
// 休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));
} finally {
// 解锁
lock.unlock();
}
}
6、synchronized 与 Lock 的对比
- synchronized是关键字,而ReentranLock是类。两者都是可重入锁(自己可以再次获取自己的内部锁)
- ReentranLock 是显示锁(手动开启和关闭锁,切记不能忘记关闭锁);而synchronized是隐式锁,出了作用域自动释放。
- 二者的机制不一样,ReentranLock底层调用的是unsafe的park方法加锁;synchronized操作的是对象头中的mark word。对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块 > 同步方法
五、线程通信
1、线程协作
生产者消费者模式:
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
- 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待;而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
- 在生产者消费者问题中,仅有synchronized是不够的。
synchronized可阻止并非更新同一个共享资源,实现了同步;但是不能用来实现不同线程之间的消息传递(通信)。
解决线程之间的通信问题的方法:
- wait():表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。
- wait(long timeout):指定等待的毫秒数
- notify():唤醒一个处于等待状态的线程。也要在同步方法或同步代码块中调用,即在调用前线程必须要获取锁。执行notify()方法后当前线程不会马上释放该锁,呈等待状态的线程也不会马上获取该对象的锁,要等到执行notify()方法的线程将程序执行完,即退出同步区域后,当前线程才会释放锁,而呈等待状态所在的线程才可以获取该对象锁。
- notifyAll():唤醒同一个对象上所有调用wait()方法的线程(倒序依次唤醒),优先级别高的线程优先调度。
线程进入可运行状态:
- 调用sleep()方法后经过的时间超过了指定的休眠时间
- 线程成功获得了试图同步的监视器
- 线程正在等待某个通知,其他线程发出了通知
- 处于挂起状态的线程调用了resume方法
管道流:
- 管道流是一种特殊的流,用于在不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输出管道中读取数据,通过使用管道,实现不同线程间的通信,而无需借助于临时文件之类的东西。
方法sleep()和方法wait()的区别:
- sleep()是Thread类中的方法,wait()是Object类中的方法。
- sleep()可以不结合synchronized使用,而wait()必须结合。
- sleep()在执行时不会释放锁,而wait()在执行后锁被释放了。
- sleep()方法执行后线程的状态是TIMED_WAITING,wait()方法执行后线程的状态是等待。
解决方式1:管程法
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。
- 生产者:负责生产数据的模块(可能是方法、对象、线程、进程);
- 消费者:负责处理数据的模块(可能是方法、对象、线程、进程);
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
// 生产者消费者模式 -- > 利用缓冲区解决:管程法
// 生产者、消费者、产品、缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
// 生产者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
// 生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了 " + i + "只鸡");
}
}
}
// 消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了 --》 " + container.pop().id + "只鸡");
}
}
}
// 产品
class Chicken {
public int id;
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,就需要等待消费者消费
if (count == chickens.length) {
// 通知消费者消费,生产等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,我们就需要放入产品
chickens[count] = chicken;
count++;
// 可以通知消费者消费
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop() {
// 判断能否消费
if (count == 0) {
// 等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
解决方式2:信号灯法
// 生产者消费者模式 -- > 信号灯法,标志位解决
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
// 生产者 --》 演员
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("快乐播放中");
} else {
this.tv.play("广告!");
}
}
}
}
// 消费者 --》 观众
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 产品 -- 》 节目
class TV {
// 演员表演,观众等待
// 观众观看,演员等待
String voice; // 表演节目
boolean flag = true;
// 表演
public synchronized void play(String voice) {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
// 通知观众观看
this.notifyAll(); // 通知唤醒
this.voice = voice;
this.flag = !this.flag;
}
// 观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("观看了:" + voice);
// 通知演员表演
this.notifyAll(); // 通知唤醒
this.flag = !this.flag;
}
}
}