Java进程与线程归纳
接触java开发或者Android开发的时候,必不可少的会接触到进程、线程这样的概念和知识,那么进程和线程到底是什么,又有什么样的关联以及有什么特点呢?
概述
进程和线程,分别对应的英文单词是Process和Thread。
先说进程,我们一般使用的电脑或者手机在运行的时候,运行的每一个程序之间相互不影响,当一个程序异常崩溃之后,其他程序可以继续执行。简单来理解,每一个程序运行就是一个进程的开启。尤其是Android系统,正常来说会给每一个app分配单独的进程,赋予一定大小的内存资源,确保每一个app都是单独运营,彼此不影响。就相当于操作系统在你运行程序的时候,给你划分了一块地方,接下来你的程序就在这块地方自己蹦跶,不要出来扰乱别人的地方。所以进程之间是相互独立的,资源不共享的。(Android因为每一个程序分配的内存有限,超过内存上限就会OOM,所以有一些app功能较多,为了更好的运行,会申请另一个进程,这样一个app程序就可能对应多个进程,这个后续再说Android多进程相关的内容)
线程呢就是在这个进程中,处理一个一个任务的,在线程的地盘上,你可以买汽水,可以吃饭、可以看电影等等。这些都是一个一个的线程。线程算作轻量级的进程,是进程的一个执行单元,是CPU调度和分派的最小单元,一个进程是有一个或多个线程组成的,同一个进程的线程之间可以共享该进程的资源,同时一个线程可以创建、撤销另一个线程,比如你吃饭的时候可以去看电影或者取消播放电影。
特点和联系
通过以上描述,进程和线程的特点也大概提到了,在说明具体的特点前,我们需要有以下基本认识:
- 目前所有的操作系统都是支持多任务处理的
- 对于一个CPU来说,某一时间点只能处理一件事情
- 处理器执行速度较快,很多事情可以被快速轮换处理,所以在我们感知上只同时被处理的
进程的特点:
- 独立性
每一个进程都是独立的地盘、资源,相互之间不干涉。
- 动态性
进程是程序的执行过程,所以就会有被创建、被执行、暂停、撤销、销毁等过程和状态。
- 并发性
并发性也可以理解为异步,多个进程在系统的执行需要由系统进程管理统一调度以异步的方式通过一定的算法来保证各个进程能够协同共享使用CPU资源和其他资源。
线程的特点
- 最小性
线程是CPU调度和分配的最小单元。
- 动态性
线程也是经过创建、执行、挂起、销毁等过程和状态。
- 关联性
线程依赖于进程,每一个线程必然有一个父进程。
- 独立性
线程之间也是独立的,一个线程不知道父进程中是否有其他线程。
- 并发行
线程的执行是抢占式的,每一个线程在执行中都可能被挂起,腾出资源来执行另一个线程。
区别和关联
-
进程是资源分配的基本单位(划分地盘),线程是处理器调度的基本单位(调度CPU等)。
-
线程可以启动、撤销另一个线程,一个进程可以有多个线程抢占式式并发执行,每一个线程在执行中都可能被挂起,腾出处理器来执行另一个线程。
-
线程不能独立执行,线程依赖于进程,一个线程必然有一个父进程,一个程序至少有一个进程,一个进程至少有一个主线程,同一个父进程的线程共享该进程资源(地盘)。
-
线程之间是独立的,一个线程不知道父进程中是否有其他的线程存在。
-
调动使用CPU的是线程,在处理器运行的是线程。
-
同一进程的多个线程共享全局变量、静态变量,堆存储,每一个线程也有自己栈段,用来存储局部或临时变量。
进程的创建、销毁
创建
java中的进程类是Process。前面已经提到,每一个程序的运行基本上都会开启一个进程,然后执行进程的主线程。所以很多时候,我们并不直接去使用代码来创建一个进程,而是交由系统来为每一个程序自动创建进程。执行Java程序都伴随着Java虚拟机的执行,每一个程序都对应着一个虚拟机,可以理解为每一个虚拟机的启动就是一个进程的启动(当然其他语言就不是虚拟机,但是也有进程的概念),然后启动该进程的主线程,由主线程启动其他线程。对应的就是可执行的Java程序中必不可少的一个main方法,main方法是程序的起点,是程序的初始线程,其他线程都由它启动。(Android中启动机制和普通的java程序不一样,所以没有main方法,此处不多说)
通过代码启动一个进程的方法:在java.lang.Runtime类和Java.lang.ProcessBuilder可以来创建一个本地的进程。
具体的使用方法如下(Java1.8):
//打开指定路径的文件夹
Process p1 =Runtime.getRuntime().exec("open /Users/anonyper/Desktop/未命名文件夹");
Process p2 = new ProcessBuilder("open", "/Users/anonyper/Desktop/未命名文件夹").start();
- Runtime.getRuntime()的其他方法如下:
Process exec(String command)
Process exec(String [] cmdarray)
Process exec(String [] cmdarrag, String [] envp)
Process exec(String [] cmdarrag, String [] envp, File dir)
Process exec(String cmd, String [] envp)
Process exec(String command, String [] envp, File dir)
//可以传入不同的参数、环境变量、以及定义执行的目录
- Java.lang.ProcessBuilder的方法如下
public ProcessBuilder(String... var1) {...}
public ProcessBuilder(List<String> var1) {...}
public ProcessBuilder command(List<String> var1) {...}
public ProcessBuilder command(String... var1) {...}
//使用ProcessBuilder可以预先配置相关属性再创建进程,而且也可以在后续使用中随着需要改变相关属性,属性修改之后对已创建的进程没影响,只影响后续的start方法创建的进程。
销毁
只要java虚拟机中还有普通线程(用户线程)在执行,那么虚拟机就不会停止,当没有普通线程时,虚拟机中都是守护线程的话,则虚拟机(该进程)就会退出。
- 自行退出
我们创建一个基本的可执行的Java程序,该程序中用户线程执行完之后,该进程也就退出了。
//代码一
public class ProcessTest {
public static void main(String[] args) {
System.out.print("我是main 方法");
}
}
执行结果:
我是main 方法
Process finished with exit code 0 //可以看到,进程以code 0的结果结束了
//代码二
public class ProcessTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
System.out.println("我是用户线程2,用来计数的:" + index);
if (index == 5)
return;
}
}
}).start();
String name = null;
name.length();
System.out.println("我是main 方法");
}
}
执行结果:
Exception in thread "main" java.lang.NullPointerException
at com.test.java.ProcessTest.main(ProcessTest.java:29)
我是用户线程2,用来计数的:1
我是用户线程2,用来计数的:2
我是用户线程2,用来计数的:3
我是用户线程2,用来计数的:4
我是用户线程2,用来计数的:5
Process finished with exit code 1 // main线程遇到异常退出了,但是整个进程还未退出,直到线程2退出了才退出
- 手动结束
可以通过java.lang.System.exit(int code)方法结束(退出code为0代表正常退出,为其他代表异常):
//代码一
public class ProcessTest {
public static void main(String[] args) {
System.out.print("11111");
System.exit(0);
System.out.print("22222");
}
}
执行结果:
11111
Process finished with exit code 0 // 可以看到,没有打印最后的“22222”进程就退出了
//代码二
public class ProcessTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
System.out.println("我是用户线程2,用来计数的:" + index);
if (index == 5)
return;
}
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
System.out.println("我是main 方法");
}
}
执行结果:
我是用户线程2,用来计数的:1
Process finished with exit code 0 // 线程2还未执行完,整个进程就退出了
线程的创建和销毁
创建和启动
java中的线程是Thread类,所以所有的线程对象都是Thread类或者其子类。java.lang.Thread类如下:
public class Thread implements Runnable {
...
private Runnable target;
...
public static native void sleep(long var0) throws InterruptedException;
...
private void init(ThreadGroup var1, Runnable var2, String var3, long var4) {
this.init(var1, var2, var3, var4, (AccessControlContext)null);
}
private void init(ThreadGroup var1, Runnable var2, String var3, long var4, AccessControlContext var6) {
...
this.target = var2;
...
}
}
...
public Thread() {
this.init((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);
}
public Thread(Runnable var1) {
this.init((ThreadGroup)null, var1, "Thread-" + nextThreadNum(), 0L);
}
Thread(Runnable var1, AccessControlContext var2) {
this.init((ThreadGroup)null, var1, "Thread-" + nextThreadNum(), 0L, var2);
}
public Thread(ThreadGroup var1, Runnable var2) {
this.init(var1, var2, "Thread-" + nextThreadNum(), 0L);
}
public Thread(String var1) {
this.init((ThreadGroup)null, (Runnable)null, var1, 0L);
}
public Thread(ThreadGroup var1, String var2) {
this.init(var1, (Runnable)null, var2, 0L);
}
public Thread(Runnable var1, String var2) {
this.init((ThreadGroup)null, var1, var2, 0L);
}
public Thread(ThreadGroup var1, Runnable var2, String var3) {
this.init(var1, var2, var3, 0L);
}
public Thread(ThreadGroup var1, Runnable var2, String var3, long var4) {
this.init(var1, var2, var3, var4);
}
...
public void run() {
if(this.target != null) {
this.target.run();
}
}
...
}
Runnable接口代码:
package java.lang;
@FunctionalInterface
public interface Runnable {
void run();
}
通过以上代码,可以看出来,想要创建一个线程有以下两种方法:
- 通过继承Thread类或其子类,在run方法中加入自己的逻辑代码,实例化该类对象来创建Thread对象
public class ThreadTest {
public static void main(String[] args) {
System.out.println("当前线程name: " + Thread.currentThread().getName());
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("当前线程name: " + getName());
}
}
//执行结果:
当前线程name: main
当前线程name: Thread-0
Process finished with exit code 0
- 实现Runnable接口,在run方法中加入自己的逻辑代码,将其传递给Thread构造函数,来创建Thread对象
public class ThreadTest {
public static void main(String[] args) {
System.out.println("当前线程name: " + Thread.currentThread().getName());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前线程name: " + Thread.currentThread().getName());
}
}).start();
}
}
//执行结果:
当前线程name: main
当前线程name: Thread-0
Process finished with exit code 0
- RunnableFuture接口是Runnable接口的子类,可以通过RunnableFuture的实现类FutureTask来作为Thread的target。
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
...
public FutureTask(Callable<V> var1) {
if(var1 == null) {
throw new NullPointerException();
} else {
this.callable = var1;
this.state = 0;
}
}
public FutureTask(Runnable var1, V var2) {
this.callable = Executors.callable(var1, var2);
this.state = 0;
}
...
}
public interface Callable<V> {
V call() throws Exception;
}
通过以上代码,我们可以实现一个Callable接口,实现起call方法作为FutureTask的执行体,然后再将该FutureTask对象作为Thread的target以此来实现线程:
public class ThreadTest {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
sleep(5000);
System.out.println("FutureTask 等待5秒后返回100");
return 100;
}
});
Thread thread = new Thread(futureTask, "futureTask");
thread.start();
try {
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
System.out.println(futureTask.get());
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//执行结果:
futureTask.isCancelled():false
futureTask.isCancelled():false
FutureTask 等待5秒后返回100
100
futureTask.isCancelled():false
futureTask.isCancelled():true
Process finished with exit code 0 // 结果值在5秒后返回,后续的逻辑代码才执行
public class ThreadTest {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
sleep(5000);
System.out.println("FutureTask 等待5秒后返回100");
String name = null;
name.length();
return 100;
}
});
Thread thread = new Thread(futureTask, "futureTask");
thread.start();
try {
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
System.out.println(futureTask.get());
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//执行结果:
futureTask.isCancelled():false
futureTask.isCancelled():false
java.util.concurrent.ExecutionException: java.lang.NullPointerException
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at com.test.java.ThreadTest.main(ThreadTest.java:33)
Caused by: java.lang.NullPointerException
at com.test.java.ThreadTest$1.call(ThreadTest.java:24)
at com.test.java.ThreadTest$1.call(ThreadTest.java:18)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.lang.Thread.run(Thread.java:745)
FutureTask 等待5秒后返回100
Process finished with exit code 0
//修改代码:
try {
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
//System.out.println(futureTask.get());
System.out.println("futureTask.isCancelled():" + futureTask.isCancelled());
System.out.println("futureTask.isCancelled():" + futureTask.isDone());
} catch (Exception e) {
e.printStackTrace();
}
//执行结果:
futureTask.isCancelled():false
futureTask.isCancelled():false
futureTask.isCancelled():false
futureTask.isCancelled():false
FutureTask 等待5秒后返回100
Process finished with exit code 0
Callable接口相比Runnable接口,call方法会有返回值,并且在程序错误时抛出异常。在使用futureTask.get()获取返回值时,当前线程会等待结果的返回后才会往下执行。如果call方法抛出异常,那么调用futureTask.get()的地方就需要捕获异常,要不当前线程就会中断。
总结
以上两种(callable和runnable算一种)方法区别是通过继承Thread类的方法创建线程时,多个线程之间无法共享线程类内的实例变量,但是通过Runnable接口创建的线程,因为runnable接口只是作为Thread对象的一个target,然后调用runnable接口的run方法作为线程执行体,多个线程可以共享一个target,这样多个线程之间就可以共享一个实例变量了。同时使用接口的方式,该线程类除了实现Runnable和Callable接口外,还可以继承其他类。
public class ThreadTest {
public static void main(String[] args) {
SellRunnable sellRunnable = new SellRunnable();
Thread thread1 = new Thread(sellRunnable, "1");
Thread thread2 = new Thread(sellRunnable, "2");
thread1.start();
thread2.start();
}
}
class SellRunnable implements Runnable {
//有十张票
int index = 10;
public synchronized void sell() {
index--;
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余:" + index);
}
@Override
public void run() {
while (index > 0) {
sell();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//执行结果:
售货窗口:1 卖出了一张票,剩余:9
售货窗口:2 卖出了一张票,剩余:8
售货窗口:2 卖出了一张票,剩余:7
售货窗口:1 卖出了一张票,剩余:6
售货窗口:1 卖出了一张票,剩余:5
售货窗口:2 卖出了一张票,剩余:4
售货窗口:1 卖出了一张票,剩余:3
售货窗口:2 卖出了一张票,剩余:2
售货窗口:1 卖出了一张票,剩余:1
售货窗口:2 卖出了一张票,剩余:0
Process finished with exit code 0
线程的结束
每一个线程都是一个顺序执行的代码段,线程的结束有以下几种方式:
- run所有代码逻辑执行完成,线程结束
- 线程在执行过程中抛出了一个异常或者error,线程结束
- 调用了该线程的stop、resume、suspend或者Runtime.runFinalizersOnExit这几个暴力方法,线程结束
使用stop等方法虽然可以强制结束线程,但是就如突然关掉电脑电源一样,这个操作是不安全的,它不会保证线程的资源被正确释放,所以这些方法已被废弃。
- 使用interrupt方法终端线程
使用interrupt方法不会真正的让线程停止,调用之后thread对象的isInterrupted()或Thread.interrupted()方法返回一个false,通过这个变量我们设置break或者return的方式使得线程退出。
使用interrupt方法会有两种情况,一种就是线程处于阻塞状态,比如sleep中,或者因为同步锁的原因等待其他线程释放资源,如果这是调用interrupt就会抛出InterruptException异常,捕获这个异常后可以退出线程。另一种情况就是没有阻塞的线程,需要获取isInterrupted()/Thread.interrupted()值判断是否需要退出线程,其实原理和第一种逻辑代码执行完退出一样。
public class ThreadTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (index <= 3) {
System.out.println("线程1 测试代码执行完毕" + " index = " + index);
index++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "线程1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (index <= 5) {
System.out.println("线程2 测试异常" + " index = " + index);
index++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 2) {
String name = null;
name.length();
}
}
}
}, "线程2");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (index <= 15) {
System.out.println("线程3 测试stop" + " index = " + index);
index++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
}
}, "线程3");
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
int index = 0;
while (!Thread.interrupted()) {
index++;
}
System.out.println("线程4 测试interrupt" + " index = " + index);
}
}, "线程4");
//启动线程
thread1.start();
thread2.start();
thread3.start();
thread4.start();
//标示是否打印过已结束判断
boolean flag1 = false;
boolean flag2 = false;
boolean flag3 = false;
boolean flag4 = false;
//标示是否调用stop方法
boolean stop_flag = false;
while (true) {
boolean thread_flag1 = thread1.isAlive();
boolean thread_flag2 = thread2.isAlive();
if (!thread_flag1 && !flag1) {
flag1 = true;
System.out.println("!!!!线程1结束了");
}
if (!thread_flag2 && !flag2) {
flag2 = true;
System.out.println("!!!!线程2结束了");
}
if (!thread_flag1 && !thread_flag2 && !stop_flag) {
stop_flag = true;
System.out.println("调用stop和interrupt方法");
thread3.stop();
thread4.interrupt();
}
boolean thread_flag3 = thread3.isAlive();
boolean thread_flag4 = thread4.isAlive();
if (!thread_flag3 && !flag3) {
flag3 = true;
System.out.println("!!!!线程3结束了");
}
if (!thread_flag4 && !flag4) {
flag4 = true;
System.out.println("!!!!线程4结束了");
}
if (!thread_flag1 && !thread_flag2 && !thread_flag3 && !thread_flag4) {
System.out.println("!!!!所有线程都结束了");
return;
}
}
}
}
//执行结果
线程1 测试代码执行完毕 index = 0
线程2 测试异常 index = 0
线程3 测试stop index = 0
线程1 测试代码执行完毕 index = 1
线程2 测试异常 index = 1
线程3 测试stop index = 1
线程3 测试stop index = 2
线程1 测试代码执行完毕 index = 2
!!!!线程2结束了
Exception in thread "线程2" java.lang.NullPointerException
at com.test.java.ThreadTest$2.run(ThreadTest.java:42)
at java.lang.Thread.run(Thread.java:745)
线程3 测试stop index = 3
线程1 测试代码执行完毕 index = 3
线程3 测试stop index = 4
!!!!线程1结束了
调用stop和interrupt方法
!!!!线程3结束了
线程4 测试interrupt index = 750987062
!!!!线程4结束了
!!!!所有线程都结束了
Process finished with exit code 0
以上,就是进程、线程的概念以及创建和结束方法。除了这些基本的方法外,还是需要了解关于线程的之间数据的共享、同步等知识点。
线程常用知识点汇总
生命周期及状态变迁
-
线程的生命周期包括:新建、就绪、运行、阻塞、死亡这几个状态。
-
当线程被new之后,处于新建状态,并不是立马就被执行了,此时线程被虚拟机分配了内存以及初始化了线程内的变量。
-
调用thread对象的start方法时,线程处于就绪状态,就绪的线程并不一定立马就被执行,仅仅只是创建了方法栈和计数器,需要等待JVM根据优先级、算法等规则调度之后才会进入运行状态。
-
线程运行之后,因为前文说过的,多个线程之间通过轮流切换的方式使用CPU资源,所以JVM将CPU资源调度给别的线程后,当前线程就从运行状态转为就绪状态。
-
当线程调用sleep方法、或者执行一个阻塞时的IO方法且没有返回结果时、调用了suspend(挂起)方法、调用一个同步对象但当前对象正在被其他线程锁持有时,线程就会进入阻塞状态。
-
上文说到的几种方法,可以使线程进入死亡状态,该线程就结束了。
状态过程相关注意事项
- 线程只能调用start方法,不能调用run方法,调用run方法只是将thread对象当作一个普通的对象来运行。
- 除了新建状态外,其他状态不能再调用start方法,否则会抛出IllegalThreadStateException异常。
- Thread.sleep()方法会使当前正在被执行的线程睡眠,让CPU可以去启动另一个处于就绪的线程,但是该方法在睡眠期间被唤醒会抛出InterruptedException异常。(上文停止线程也用到该原理)
- 当时sleep时间结束后、调用的IO方法等待返回结果时、获取同步锁对象时、挂起的线程调用了resume方法时该线程就出于就绪状态,等待CPU调度。
- 主线程执行完之后,不影响其他线程的运行,每一个线程运行起来后,级别相同。
- 线程分守护线程和普通线程,守护线程时JVM自己使用的线程,如垃圾回收线程,用户创建的线程基本上为普通线程,也可以自动设置Thread对象的setDaemon(true)将线程改为守护线程(在start之前调用)。Thread对象的isDaemon方法可以查询是否时守护线程。
- 用户创建的线程设置为守护线程后,不管是否执行完,当进程中所有普通线程执行完,整个进程也就结束。
public class ThreadTest {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
sleep(50000);
System.out.println("FutureTask 等待50秒后返回100");
return 100;
}
});
Thread thread = new Thread(futureTask, "futureTask");
thread.setDaemon(true);
thread.start();
System.out.println("主线程执行完毕");
}
}
//执行结果:
主线程执行完毕
Process finished with exit code 0
- Thread对象的setPriority(int newPriority)可以设置线程优先级,范围从0~10,值越大,级别越高,级别越高,执行的机会就越多,不设置的话,默认级别和其父进程级别相同。
线程sleep和yield方法区别
- sleep方法会让线程进入阻塞状态,在其睡眠时间内,该线程不会被执行。
- yield方法只是将线程处于就绪状态,可能调度器又立马调度了该线程。
- yield方法暂停后,和该线程优先级相同或者高的线程优先执行。
- sleep之后,其他线程执行的机会有JVM决定,不一定就是高优先级的。
- yield不会抛出异常,sleep会抛出InterruptedException异常。
public class ThreadTest {
public static void main(String[] args) {
SellRunnable sellRunnable = new SellRunnable();
Thread thread1 = new Thread(sellRunnable, "1");
thread1.setPriority(1);
Thread thread2 = new Thread(sellRunnable, "2");
thread2.setPriority(5);
Thread thread3 = new Thread(sellRunnable, "3");
thread3.setPriority(5);
thread2.start();
thread1.start();
thread3.start();
}
}
class SellRunnable implements Runnable {
//有十张票
int index = 10;
public synchronized void sell() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(index>=1){
index--;
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余:" + index);
}else{
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 买票时没票了");
}
}
@Override
public void run() {
while (index > 0) {
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 开始买票");
sell();
}
}
}
//执行结果:
售货窗口:2 开始买票
售货窗口:3 开始买票
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:9
售货窗口:2 开始买票
售货窗口:1 卖出了一张票,剩余:8
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:7
售货窗口:3 开始买票
售货窗口:3 卖出了一张票,剩余:6
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:5
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:4
售货窗口:2 开始买票
售货窗口:2 卖出了一张票,剩余:3
售货窗口:2 开始买票
售货窗口:2 卖出了一张票,剩余:2
售货窗口:2 开始买票
售货窗口:2 卖出了一张票,剩余:1
售货窗口:2 开始买票
售货窗口:2 卖出了一张票,剩余:0
售货窗口:1 买票时没票了
售货窗口:3 买票时没票了
Process finished with exit code 0 //可以看出,2、3执行的相对较多
//加入sleep
@Override
public void run() {
while (index > 0) {
try {
Thread.sleep(100);
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 开始买票");
sell();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//执行结果:
售货窗口:1 开始买票
售货窗口:2 开始买票
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:9
售货窗口:3 卖出了一张票,剩余:8
售货窗口:2 卖出了一张票,剩余:7
售货窗口:3 开始买票
售货窗口:2 开始买票
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:6
售货窗口:1 卖出了一张票,剩余:5
售货窗口:2 卖出了一张票,剩余:4
售货窗口:3 开始买票
售货窗口:2 开始买票
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:3
售货窗口:1 卖出了一张票,剩余:2
售货窗口:2 卖出了一张票,剩余:1
售货窗口:3 开始买票
售货窗口:2 开始买票
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:0
售货窗口:1 买票时没票了
售货窗口:2 买票时没票了
Process finished with exit code 0 // sleep之后,各个窗口差不多
//加入yield
@Override
public void run() {
while (index > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 开始买票");
sell();
if ("2".equals(Thread.currentThread().getName())) {
Thread.yield();
}
}
}
//执行结果:
售货窗口:2 开始买票
售货窗口:3 开始买票
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:9
售货窗口:2 开始买票
售货窗口:1 卖出了一张票,剩余:8
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:7
售货窗口:1 卖出了一张票,剩余:6
售货窗口:3 开始买票
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:5
售货窗口:3 开始买票
售货窗口:2 卖出了一张票,剩余:4
售货窗口:2 开始买票
售货窗口:3 卖出了一张票,剩余:3
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:2
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:1
售货窗口:3 开始买票
售货窗口:2 卖出了一张票,剩余:0
售货窗口:3 买票时没票了
售货窗口:1 买票时没票了
Process finished with exit code 0 // 2变少,相对3变多(执行太快,yield看不出效果)
线程同步
多线程之间调用同一对象时,为了运行的安全和准确性,需要对该对象进行同步,确保每一个线程用到的时候该对象的结果都是正确的,该对象的状态都是合理的,这部分涉及到同步、线程锁等知识点。这部分的只是就涉及到了synchronized、同步锁(Lock)的概念。
synchronized
synchronized关键词可以修饰对象、方法,通常用法如下:
//同步代码块
synchronized(Object object){
...
}
//或者
//同步方法
public synchronized void test(){
...
}
其中有一个同步监视器的概念,比如上面同步代码块的object对象以及同步方法的this对象就会同步监视,多个线程同时调用一个同步的代码块或者方法时,在任何时刻只能够一个线程能够获得该同步监视的对象锁,执行完代码之后才会释放该锁,在此期间其他调用的线程只能等待该锁被释放后才能调用。
上文中提到的SellRunnable类中的sell方法也用到了synchronized,上文中代码执行太快,所以感知不到,如果修改一下就能明白有没有synchronized的区别了。
public class ThreadTest {
public static void main(String[] args) {
SellRunnable sellRunnable = new SellRunnable();
Thread thread1 = new Thread(sellRunnable, "1");
Thread thread2 = new Thread(sellRunnable, "2");
Thread thread3 = new Thread(sellRunnable, "3");
thread2.start();
thread1.start();
thread3.start();
}
}
class SellRunnable implements Runnable {
//有十张票
int index = 10;
public void sell() {
if (index >= 1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
index--;
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余:" + index);
} else {
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 买票时没票了");
}
}
@Override
public void run() {
while (index > 0) {
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 开始买票");
sell();
}
}
}
//执行结果:
售货窗口:1 开始买票
售货窗口:2 开始买票
售货窗口:3 开始买票
售货窗口:2 卖出了一张票,剩余:9
售货窗口:2 开始买票
售货窗口:1 卖出了一张票,剩余:9
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:8
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:6
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:6
售货窗口:2 开始买票
售货窗口:3 卖出了一张票,剩余:5
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:4
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:3
售货窗口:3 卖出了一张票,剩余:2
售货窗口:3 开始买票
售货窗口:2 开始买票
售货窗口:3 卖出了一张票,剩余:1
售货窗口:2 卖出了一张票,剩余:0
售货窗口:1 卖出了一张票,剩余:1
Process finished with exit code 0 //可以看到,票数减少是错误的
//sell方法添加synchronized修饰符后 执行结果:
public synchronized void sell() {
if (index >= 1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
index--;
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余:" + index);
} else {
System.out.println("售货窗口:" + Thread.currentThread().getName() + " 买票时没票了");
}
}
售货窗口:2 开始买票
售货窗口:3 开始买票
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:9
售货窗口:2 开始买票
售货窗口:1 卖出了一张票,剩余:8
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:7
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:6
售货窗口:1 开始买票
售货窗口:2 卖出了一张票,剩余:5
售货窗口:2 开始买票
售货窗口:1 卖出了一张票,剩余:4
售货窗口:1 开始买票
售货窗口:1 卖出了一张票,剩余:3
售货窗口:1 开始买票
售货窗口:3 卖出了一张票,剩余:2
售货窗口:3 开始买票
售货窗口:1 卖出了一张票,剩余:1
售货窗口:1 开始买票
售货窗口:1 卖出了一张票,剩余:0
售货窗口:2 买票时没票了
售货窗口:3 买票时没票了
Process finished with exit code 0 // 可以看到,票数是正常减少的
以上对于sell方法进行同步之后,在某一瞬间,只会有一个线程调用该方法,所以里面判断index的时候得到的结果就是正确的结果。
以上同步的时候,是以降低运行效率的方式来保证线程安全的,为此,不要对线程使用类中没必要的方法、对象进行同步标识,只对有竞争的资源或者代码进行同步标识。
同步标识后,有以下几点可以释放该锁:
- 代码块、方法执行完毕(正常完毕、return或break、抛出异常)
- 调用了wait方法,使得当前线程暂停。
当线程执行到同步代码块时,sleep、yield方法不会释放该同步锁,挂起方法suspend也不会(线程操作过程中尽量避免使用suspend、resume来操作线程状态,容易导致死锁。)
同步锁Lock
上文中提到的synchronized是java中的一个关键词,也提到了在sleep的时候、进行IO操作的时候该线程不会释放线程锁,其他线程就需要一直等待,这样有时会降低执行的效率,所以就需要一个可以在线程阻塞时可以释放线程锁的替代方案,Lock就是为了解决这个问题出现的。
Lock是一个java中的类,在java.util.concurrent.locks包中,具体的代码如下:
public interface Lock {
void lock();//加锁
void lockInterruptibly() throws InterruptedException;//加锁
boolean tryLock();//加锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加锁
void unlock();//释放锁
Condition newCondition();//线程协作中用到
}
Lock接口的一个实现子类为ReentrantLock,在java.util.concurrent.locks包下,ReentrantLock的源代码如下:
public class ReentrantLock implements Lock, Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final ReentrantLock.Sync sync;
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}
public ReentrantLock(boolean var1) {//是否创建公平锁
this.sync = (ReentrantLock.Sync)(var1?new ReentrantLock.FairSync():new ReentrantLock.NonfairSync());
}
public void lock() {
this.sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
this.sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return this.sync.nonfairTryAcquire(1);
}
public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException {
return this.sync.tryAcquireNanos(1, var3.toNanos(var1));
}
public void unlock() {
this.sync.release(1);
}
public Condition newCondition() {
return this.sync.newCondition();
}
public int getHoldCount() {//当前线程持有该锁的数量
return this.sync.getHoldCount();
}
public boolean isHeldByCurrentThread() {//该锁是否被当前线程持有
return this.sync.isHeldExclusively();
}
public boolean isLocked() {//是否被其他线程持有该锁
return this.sync.isLocked();
}
public final boolean isFair() {//是否是公平锁
return this.sync instanceof ReentrantLock.FairSync;
}
protected Thread getOwner() {//当前锁的持有线程
return this.sync.getOwner();
}
public final boolean hasQueuedThreads() {//是否有线程在等待该锁
return this.sync.hasQueuedThreads();
}
public final boolean hasQueuedThread(Thread var1) {//目标线程是否在等待该锁
return this.sync.isQueued(var1);
}
public final int getQueueLength() {//等待该锁线程的数量
return this.sync.getQueueLength();
}
protected Collection<Thread> getQueuedThreads() {//获取所有等待该锁的线程集合
return this.sync.getQueuedThreads();
}
...
}
Lock的使用方法
- lock
lock() 用来获取锁,如果该锁被其他线程占用,则进入等待。
public class LockTest {
public static void main(String[] args) {
com.test.java.SellRunnable sellRunnable = new com.test.java.SellRunnable();
Thread thread1 = new Thread(sellRunnable, "1号窗口");
Thread thread2 = new Thread(sellRunnable, "2号窗口");
Thread thread3 = new Thread(sellRunnable, "3号窗口");
thread1.start();
thread2.start();
thread3.start();
}
}
public class SellRunnable implements Runnable {
//有十张票
int index = 10;
Lock lock = new ReentrantLock();
public void sell() {
try {
lock.lock();
System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++");
if (index >= 1) {
index--;
System.out.println("售货柜台:" + Thread.currentThread().getName() + "卖出了一张票,剩余:" + index);
} else {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "买票时没票了000");
}
} finally {
lock.unlock();
}
}
@Override
public void run() {
while (index > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sell();
}
}
}
运行结果:
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:9
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:8
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:7
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:6
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:5
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:4
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:3
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:2
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:1
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:0
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口买票时没票了000
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口买票时没票了000
Process finished with exit code 0 //每一个窗口都随机获取票源、然后卖出票
- tryLock
tryLock()尝试获取锁,如果获取成功返回true,如果失败,则返回false,不会进入等待状态。
public class SellRunnable implements Runnable {
//有十张票
int index = 10;
Lock lock = new ReentrantLock();
public void sell() {
if (lock.tryLock()) {
try {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++");
if (index >= 1) {
index--;
System.out.println("售货柜台:" + Thread.currentThread().getName() + "卖出了一张票,剩余:" + index);
} else {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "买票时没票了000");
}
} finally {
lock.unlock();
}
} else {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "没有获取票源!!!");
}
}
@Override
public void run() {
while (index > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sell();
}
}
}
运行结果:
售货柜台:1号窗口获取了票源+++++
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口没有获取票源!!!
售货柜台:1号窗口卖出了一张票,剩余:9
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:8
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:7
售货柜台:1号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:6
售货柜台:1号窗口获取了票源+++++
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口卖出了一张票,剩余:5
售货柜台:2号窗口获取了票源+++++
售货柜台:1号窗口没有获取票源!!!
售货柜台:2号窗口卖出了一张票,剩余:4
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口获取了票源+++++
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口卖出了一张票,剩余:3
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:2
售货柜台:2号窗口获取了票源+++++
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口卖出了一张票,剩余:1
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:0
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口没有获取票源!!!
Process finished with exit code 0//没有获取到货源的票口,就直接没有等待,进入下次买票
- tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)可以设置拿不到锁的时候等待一段时间。//第一个参数时常长,第二个参数时间单位
public class SellRunnable implements Runnable {
//有十张票
int index = 10;
Lock lock = new ReentrantLock();
public void sell() {
try {
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++");
if (index >= 1) {
index--;
System.out.println("售货柜台:" + Thread.currentThread().getName() + "卖出了一张票,剩余:" + index);
} else {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "买票时没票了000");
}
try {
Thread.sleep(2000);//人为加入买票时间
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
} else {
System.out.println("售货柜台:" + Thread.currentThread().getName() + "没有获取票源!!!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (index > 0) {
try {
Thread.sleep(500);//要不执行太快,看不出效果
} catch (InterruptedException e) {
e.printStackTrace();
}
sell();
}
}
}
执行结果:
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:9
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:8
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口没有获取票源!!!
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:7
售货柜台:1号窗口没有获取票源!!!
售货柜台:2号窗口没有获取票源!!!
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:6
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:5
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口没有获取票源!!!
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:4
售货柜台:1号窗口没有获取票源!!!
售货柜台:2号窗口没有获取票源!!!
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:3
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:2
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口没有获取票源!!!
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:1
售货柜台:1号窗口没有获取票源!!!
售货柜台:2号窗口没有获取票源!!!
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:0
售货柜台:2号窗口没有获取票源!!!
售货柜台:3号窗口没有获取票源!!!
Process finished with exit code 0 //当买票时间大约等待时间时,则没有获取票源的窗口不买票,进入下个买票机会
将买票时间缩短:
try {
Thread.sleep(500);//人为加入买票时间
} catch (InterruptedException e) {
e.printStackTrace();
}
执行结果:
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:9
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:8
售货柜台:3号窗口没有获取票源!!!
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:7
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:6
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:5
售货柜台:3号窗口没有获取票源!!!
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:4
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:3
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:2
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:1
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:0
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口买票时没票了000
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口买票时没票了000
Process finished with exit code 0 //等待时间内获取到票源了,也就卖出票了
- lockInterruptibly
lockInterruptibly()通过该方法获取锁时,如果该锁正在被其他线程持有,则进入等待状态,但是这个等待过程是可以被中断的,通过调用Thread对象的interrupt方法就可中断等待,中断时抛出异常InterruptedException,需要捕获或者声明抛出。
public class ThreadTest {
public static void main(String[] args) {
SellRunnable sellRunnable = new SellRunnable();
Thread thread1 = new Thread(sellRunnable, "1号窗口");
Thread thread2 = new Thread(sellRunnable, "2号窗口");
Thread thread3 = new Thread(sellRunnable, "3号窗口");
thread1.start();
try {
Thread.sleep(500);//确保窗口1号先获取锁
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
thread3.start();
try {
Thread.sleep(2000);//等待两秒后,打断窗口2、3的等待
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
thread3.interrupt();
}
}
SellRunnable中等待时间加长:
try {
Thread.sleep(5000);//人为加入买票时间
} catch (InterruptedException e) {
e.printStackTrace();
}
执行结果:
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:9
售货柜台:3号窗口被打断了 //这个地方被打断了
售货柜台:2号窗口被打断了 //这个地方被打断了
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:8
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:7
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:6
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:5
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:4
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:3
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口卖出了一张票,剩余:2
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口卖出了一张票,剩余:1
售货柜台:1号窗口获取了票源+++++
售货柜台:1号窗口卖出了一张票,剩余:0
售货柜台:2号窗口获取了票源+++++
售货柜台:2号窗口买票时没票了000
售货柜台:3号窗口获取了票源+++++
售货柜台:3号窗口买票时没票了000
Process finished with exit code 0
synchronized和Lock对比
通过以上代码,可以看出Lock和synchronized的几点关联和区别:
- 两者都是可重入锁
可重入锁是指当一个线程获得对象锁之后,该线程可以再次获取该对象的锁而不被阻塞。比如同一个类中有多个方法(或一个方法递归调用)被synchronized修饰或者被Lock加持后,同一个线程在调用这两个方法时都可以获取该对象的锁而不被阻塞。
不可重入锁的示例:
public class Lock{
private boolean isLocked = false;
public void lock(){
while(isLocked){
wait();
}
isLocked = true;
}
public void unlock(){
isLocked = false;
notify();
}
}
//使用方法:
public class Test{
Lock lock = new Lock();
public void test1(){
lock.lock();
test2();
lock.unlock();
}
public void test2(){
lock.lock();
...
lock.unlock();
}
}
Test类在调用test1方法的时候,执行完lock.lock()后调用test2的时候,就会一直等待,变成死锁。
可重入锁设计原理:
public class Lock{
private boolean isLocked = false;
private Thread lockedThread = null;
int lockedCount = 0;
public void lock(){
Thread thread = Thread.currentThread();
while(isLocked && thread != lockedThread){
wait();
}
isLocked = true;
lockedCount++;
lockedThread = thread;
}
public void unlock(){
Thread thread = Thread.currentThread();
if(thread == lockedThread){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
lockedThread = null;
notify();
}
}
}
}
这样调用Test类的test1方法后,test2方法也能顺利被执行。
synchronized在实现上也基本上是采用记数器的方式来实现可重入的。
- Lock是可中断锁,synchronized不可中断。
当一个线程B执行被锁的对象的代码时,发现线程A已经持有该锁,那么线程B就会进入等待,但是synchronized就无法中断该等待过程,而Lock就可以通过lockInterruptibly方法抛出异常从而中断等待,去处理别的事情。
- Lock可创建公平锁,synchronized是非公平锁。
公平锁的意思是按照请求的顺序来获取锁,不平公锁就无法保证线程获取锁的先后次序。
- Lock可以知道是否获取到锁,synchronized不可以。
- synchronized在发生异常或者运行完毕,会自动释放线程占有的锁。而Lock需要主动释放锁,否则会锁死;
- synchronized在阻塞时,别的线程无法获取锁,Lock可以(这也是lock设计的一个目的)。
读写锁
多个线程对同一个文件进行写操作时,会发生冲突所以需要加锁,但是对同一个文件进行读操作的时候,使用上面的方法会造成效率的降低,所以基于这种情况,产生了ReadWriteLock这个接口:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();//读的锁
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();//写的锁
}
这个接口的实现类是ReentrantReadWriteLock,其源代码如下:
public class ReentrantReadWriteLock implements ReadWriteLock, Serializable {
private static final long serialVersionUID = -6992448646407690164L;
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
...
public ReentrantReadWriteLock.WriteLock writeLock() {//获取write lock
return this.writerLock;
}
public ReentrantReadWriteLock.ReadLock readLock() {//获取read lock
return this.readerLock;
}
...
}
使用方法和Lock一样,使用到write时调用writeLock()方法获取lock进行加锁,使用到read时调用readLock()方法进行加锁,需要注意的知识点如下:
- 线程A占用写锁,线程B在申请写、读的时候需要等待。
- 线程A占用读锁,线程B在申请写操作时,需要等待。
- 线程A占用读锁,线程B获取读操作时可以获取到。
总结
如果需要效率提升,则建议使用Lock,如果效率要求不高,则synchronized满足使用条件,业务逻辑写起来也简单,不需要手动释放锁。
以上,就是进程、线程的知识点总结。
网友评论