Java多线程
Java 多线程(一)、多线程的基本概念和使用
一、进程和线程的基础知识
1、进程和线程的概念
进程:运行中的应用程序称为进程,拥有系统资源(cpu、内存)
线程:进程中的一段代码,一个进程中可以有多段代码。本身不拥有资源(共享所在进程的资源)
在 java 中,程序入口被自动创建为主线程,在主线程中可以创建多个子线程。
区别:
1、是否占有资源问题
2、创建或撤销一个进程所需要的开销比创建或撤销一个线程所需要的开销大。
3、进程为重量级组件,线程为轻量级组件
多进程: 在操作系统中能同时运行多个任务(程序)
多线程: 在同一应用程序中有多个功能流同时执行
2、线程的主要特点
①、不能以一个文件名的方式独立存在在磁盘中;
②、不能单独执行,只有在进程启动后才可启动;
③、线程可以共享进程相同的内存(代码与数据)。
3、线程的主要用途
①、利用它可以完成重复性的工作(如实现动画、声音等的播放)。
②、从事一次性较费时的初始化工作(如网络连接、声音数据文件的加载)。
③、并发执行的运行效果(一个进程多个线程)以实现更复杂的功能
4、多线程(多个线程同时运行)程序的主要优点
①、可以减轻系统性能方面的瓶颈,因为可以并行操作;
②、提高 CPU 的处理器的效率,在多线程中,通过优先级管理,可以使重要的程序优先操作,提
高了任务管理的灵活性;另一方面,在多 CPU 系统中,可以把不同的线程在不同的 CPU中执行,真正
做到同时处理多任务。
二、线程的创建和启动
其实看看 API,从 Thread 的构造方法,就可以看出创建一个线程的方式:
Thread()
分配新的 Thread 对象。
Thread(Runnable target)
分配新的 Thread 对象。
Thread(Runnable target,String name)
分配新的 Thread 对象。
Thread(String name)
分配新的 Thread 对象。
Thread(ThreadGroup group,Runnable target)
分配新的 Thread 对象。
Thread(ThreadGroup group,Runnable target,String name)
分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为
group 所引用的线程组的一员。
Thread(ThreadGroup group,Runnable target,String name, long stackSize)
分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为
group 所引用的线程组的一员,并具有指定的堆栈大小。
Thread(ThreadGroup group,String name)
分配新的 Thread 对象。
后面 4个构造方法创建一个线程并加入到一个线程组中,但是创建线程的方式和前面的相似。
java 中创建一个线程有两种方式:
1、继承 Thread 类,重写 run()方法,然后直接 new 这个对象的实例,创建一个线程的实例。然
后调用 start()方法启动线程
2、实现 Runnable 接口,重写 run()方法,然后调用 new Thread(runnable)的方式创建一个线程,
然后调用 start()方法启动线程
其实看 Thread 的源文件,发现它也是实现了 Runnable 接口的。
public class Thread implements Runnable
1、继承 Thread 类的方式
public class Test1 {
public static void main(String[] args) {
(().getName());
MyThread myThread=new MyThread();
();
}
}
class MyThread extends Thread{
int i=0;
@Override
public void run() {
while (i<10) {
(()+" i 的值 "+i);
i++;
}
}
}
2、实现 Runnable 接口
public class Test1 {
public static void main(String[] args) {
(().getName());
Thread thread=new Thread(new MyRunnable());
();
}
}
class MyRunnable implements Runnable{
int i=0;
@Override
public void run() {
while (i<10) {
(().getName()+" i 的值 "+i);
i++;
}
}
}
注意:
①、在继承 Thread 的方式中,可以使用 getName()方法,来获得当前线程的名字,这是因为在
Thread类中,有这个方法。可是在实现Runnable方式中,却不可以使用(),因为Runnable
接口没有这个方法(可以看出来,因为我们没有提示我们需要重写这个方法),所以只能通过 Thread
的静态方法 ()取得当前的 Thread 对象,在调用 getName()方法,来取得当前
线程的名字。
②、对 Java 来说,run()方法没有任何特别之处。像 main()方法一样,它只是新线程知道调用
的方法名称(和签名)。因此,在 Runnable 上或者 Thread 上调用 run 方法是合法的。但并不启动新的
线程。只有调用 start()方法才会启动新线程。
3、两种方式的对比
采用实现 Runnable 接口方式的多线程具有优势,一般都会使用这种方式:
1、线程类只是实现了 Runnable 接口,还可以继承其他类。
2、在这种方式下,可以多个线程共享一个 Runnable target 对象,所以非常适合多个相同线程
来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好的体现了面向
对象的思想。
Java 多线程(二)、线程的生命周期和状态控制
一、线程的生命周期
线程状态转换图:
1、新建状态
用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新
生状态的线程有自己的内存空间,通过调用 start 方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用 start()方法,否则会出现
异常。
2、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到 CPU,处于线程就绪队列(尽管是采
用队列形式,事实上,把它称为可运行池而不是可运行队列。因为 cpu 的调度不一定是按照先进先出
的顺序来调度的),等待系统为其分配 CPU。等待状态并不是执行状态,当系统选定一个等待执行的
Thread 对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获
得 CPU,线程就进入运行状态并自动调用自己的 run 方法。
提示:如果希望子线程调用 start()方法后立即执行,可以使用 ()方式使主线程睡
眠一伙儿,转去执行子线程。
3、运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了 cpu 的调度,就会从就绪状态变为运行状态,执行 run()方法
中的任务。如果该线程失去了 cpu 资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。
也可以对在运行状态的线程调用 yield()方法,它就会让出 cpu 资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用 sleep 方法主动放弃所占用的系统资源
②、线程调用一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的 suspend 方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量
避免使用该方法。
当线程的 run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了 stop()、desyory()
方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态
处于运行状态的线程在某些情况下,如执行了 sleep(睡眠)方法,或等待 I/O 设备等资源,将
让出 CPU 并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待
的 I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止
的位置开始继续运行。有三种方法可以暂停 Threads 执行:
5、死亡状态
当线程的 run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,
但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调
用 start()方法,会抛出 异常。
二、线程状态的控制
Java 提供了一些便捷的方法用于会线程状态的控制。
可以看到很多方法,已经标注为过时的,我们应该尽可能的避免使用它们,而应该重点关注
start()、interrupt()、join()、sleep()、yield()等直接控制方法,和 setDaemon()、setPriority()
等间接控制方法。
1、线程睡眠——sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用 Thread
的 sleep 方法,从上面可以看到 sleep 方法有两种重载的形式,但是使用方法一样。
比如,我们想要使主线程每休眠 100 毫秒,然后再打印出数字:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<100;i++){
("main"+i);
(100);
}
}
}
可以明显看到打印的数字在时间上有些许的间隔。
注意如下几点问题
①、sleep 是静态方法,最好不要用 Thread 的实例对象调用它,因为它睡眠的始终是当前正在
运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。看下面的例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
(().getName());
MyThread myThread=new MyThread();
();
(1000);//这里 sleep 的就是 main 线程,而非 myThread 线程
(10);
for(int i=0;i<100;i++){
("main"+i);
}
}
}
②、Java 线程调度是 Java 多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程
序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精
准控制。因为使用 sleep 方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入
到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果
调用 (1000)使得线程睡眠 1秒,可能结果会大于 1秒。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
(()+"线程" + i + "次执行!");
try {
(50);
} catch (InterruptedException e) {
();
}
}
}
}
看某一次的运行结果:
Thread-0 线程 0次执行!
Thread-1 线程 0次执行!
Thread-1 线程 1次执行!
Thread-0 线程 1次执行!
Thread-0 线程 2次执行!
Thread-1 线程 2次执行!
可以看到,线程 0首先执行,然后线程 1执行一次,又了执行一次。可以看到它并不是按照 sleep
的顺序执行的。
2、线程让步——yield
yield()方法和 sleep()方法有点相似,它也是 Thread 类提供的一个静态的方法,它也可以让当
前正在执行的线程暂停,让出 cpu资源给其他的线程。但是和 sleep()方法不同的是,它不会进入到
阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,
让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用 yield()
方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了 yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前
线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干
涉 cpu 调度线程。
yield 的用法:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
(pro);// 设置优先级
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
(() + "线程第" + i + "次执行!");
if (i % 5 == 0)
();
}
}
}
关于 sleep()方法和 yield()方的区别如下:
①、sleep 方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。
而 yield 方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep 方法声明抛出了 InterruptedException,所以调用 sleep 方法的时候要捕获该异常,
或者显示声明抛出该异常。而 yield 方法则没有声明抛出任务异常。
③、sleep 方法比 yield 方法有更好的可移植性,通常不要依靠 yield 方法来控制并发线程的执
行。
3、线程合并——join
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必
须等待另一个线程执行完毕才能执行时,Thread 类提供了 join 方法来完成这个功能,注意,它不是
静态方法。
从上面的方法的列表可以看到,它有 3个重载的方法:
void join()
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。如果在 millis 时间内,该线程没有执行
完,那么当前线程进入就绪状态,重新等待 cpu 调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在 millis 时间内,该线程没有
执行完,那么当前线程进入就绪状态,重新等待 cpu 调度
例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
();
(1);//将主线程加入到子线程后面,
不过如果子线程在 1毫秒时间内没执行完,
则主线程便不再等待它执行完,进入就绪状态,等待 cpu 调度
for(int i=0;i<30;i++){
(().getName() + "线程第" + i + "次执
行!");
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
(() + "线程第" + i + "次执行!");
}
}
}
在这个例子中,在主线程中调用 (); 就是将主线程加入到 thread 子线程后面等待
执行。不过有时间限制,为 1毫秒。
4、线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低
的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不
过,优先级高的线程获取 CPU 资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main 线程具有
普通优先级。
Thread 类提供了 setPriority(int newPriority)和 getPriority()方法来设置和返回一个指定
线程的优先级,其中 setPriority 方法的参数是一个整数,范围是 1~·0之间,也可以使用 Thread
类提供的三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
(() + "线程第" + i + "次执行!");
}
}
}
从结果可以看到 ,一般情况下,高级线程更显执行完毕。
注意一点:虽然 Java 提供了 10 个优先级别,但这些优先级别需要操作系统的支持。不同的操作
系统的优先级并不相同,而且也不能很好的和 Java 的 10 个优先级别对应。所以我们应该使用
MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最
好的可移植性。
5、守护线程
守护线程与普通线程写法上基本么啥区别,调用线程对象的方法 setDaemon(true),则可以将其
设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM 的垃圾回收、内存管理等线程都是守护线程。
还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接
个数、超时时间、状态等等。
setDaemon 方法的详细说明:
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行
的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这
可能抛出 SecurityException(在当前线程中)。
参数:
on - 如果为 true,则将该线程标记为守护线程。
抛出:
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程
/**
* Java 线程:线程的调度-守护线程
*/
public class Test {
public static void main(String[] args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
(true); //设置为守护线程
();
();
}
}
class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
("线程 1第" + i + "次执行!");
try {
(7);
} catch (InterruptedException e) {
();
}
}
}
}
class MyDaemon implements Runnable {
public void run() {
for (long i = 0; i < 9999999L; i++) {
("后台线程第" + i + "次执行!");
try {
(7);
} catch (InterruptedException e) {
();
}
}
}
}
执行结果:
后台线程第 0次执行!
线程 1第 0次执行!
线程 1第 1次执行!
后台线程第 1次执行!
后台线程第 2次执行!
线程 1第 2次执行!
线程 1第 3次执行!
后台线程第 3次执行!
线程 1第 4次执行!
后台线程第 4次执行!
后台线程第 5次执行!
后台线程第 6次执行!
后台线程第 7次执行!
从上面的执行结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE 判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状
态,因此,在使用后台县城时候一定要注意这个问题。
守护线程的用途:
守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器
里做自动语法检查、自动保存等功能。Java 的垃圾回收也是一个守护线程。
守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景
音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出
主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
6、如何结束一个线程
()、、、 这些终止线
程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下
面的方法。
1、正常执行完 run 方法,然后结束掉
2、控制循环条件和判断条件的标识符来结束掉线程
比如说 run 方法这样写:
class MyThread extends Thread {
int i=0;
@Override
public void run() {
while (true) {
if(i==10)
break;
i++;
(i);
}
}
}
或者
class MyThread extends Thread {
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
(i);
}
}
}
或者
class MyThread extends Thread {
int i=0;
@Override
public void run() {
while (true) {
if(i==10)
return;
i++;
(i);
}
}
}
只要保证在一定的情况下,run方法能够执行完毕即可。而不是 while(true)的无线循环。
3、使用 interrupt 结束一个线程。
诚然,使用第 2中方法的标识符来结束一个线程,是一个不错的方法,但是如果,该线程是处于
sleep、wait、join 的状态的时候,while 循环就不会执行,那么我们的标识符就无用武之地了,当
然也不能再通过它来结束处于这 3种状态的线程了。
可以使用 interrupt 这个巧妙的方式结束掉这个线程。
我们看看 sleep、wait、join 方法的声明:
public final void wait() throws InterruptedException
public static native void sleep(long millis) throws InterruptedException
public final void join() throws InterruptedException
可以看到,这三者有一个共同点,都抛出了一个 InterruptedException 的异常。
在什么时候会产生这样一个异常呢?
每个 Thread 都有一个中断状状态,默认为 false。可以通过 Thread 对象的 isInterrupted()方
法来判断该线程的中断状态。可以通过 Thread 对象的 interrupt()方法将中断状态设置为 true。
当一个线程处于 sleep、wait、join 这三种状态之一的时候,如果此时他的中断状态为 true,
那么它就会抛出一个 InterruptedException 的异常,并将中断状态重新设置为 false。
看下面的简单的例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
();
}
}
class MyThread extends Thread {
int i=1;
@Override
public void run() {
while (true) {
(i);
(());
try {
("我马上去 sleep 了");
(2000);
();
} catch (InterruptedException e) {
("异常捕获了"+());
return;
}
i++;
}
}
}
测试结果:
1
false
我马上去 sleep 了
2
true
我马上去 sleep 了
异常捕获了 false
可以看到,首先执行第一次 while 循环,在第一次循环中,睡眠 2秒,然后将中断状态设置为
true。当进入到第二次循环的时候,中断状态就是第一次设置的 true,当它再次进入 sleep 的时候,
马上就抛出了 InterruptedException 异常,然后被我们捕获了。然后中断状态又被重新自动设置为
false 了(从最后一条输出可以看出来)。
所以,我们可以使用 interrupt 方法结束一个线程。具体使用如下:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
();
(3000);
();
}
}
class MyThread extends Thread {
int i=0;
@Override
public void run() {
while (true) {
(i);
try {
(1000);
} catch (InterruptedException e) {
("中断异常被捕获了");
return;
}
i++;
}
}
}
多测试几次,会发现一般有两种执行结果:
0
1
2
中断异常被捕获了
或者
0
1
2
3
中断异常被捕获了
这两种结果恰恰说明了 只要一个线程的中断状态一旦为 true,只要它进入 sleep 等状态,或者
处于 sleep 状态,立马回抛出 InterruptedException 异常。
第一种情况,是当主线程从 3秒睡眠状态醒来之后,调用了子线程的 interrupt 方法,此时子线
程正处于 sleep 状态,立马抛出 InterruptedException 异常。
第一种情况,是当主线程从 3秒睡眠状态醒来之后,调用了子线程的 interrupt 方法,此时子线
程还没有处于 sleep 状态。然后再第 3次 while 循环的时候,在此进入 sleep 状态,立马抛出
InterruptedException 异常。
Java 多线程(三)、线程同步
在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系。
可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题。现
在就来学习多线程对数据访问的控制吧。
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重
的问题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访
问。
一、多线程引起的数据访问安全问题
下面看一个经典的问题,银行取钱的问题:
1)、你有一张银行卡,里面有 5000 块钱,然后你到取款机取款,取出 3000,当正在取的时候,
取款机已经查询到你有 5000 块钱,然后正准备减去 300 块钱的时候
2)、你的老婆拿着那张银行卡对应的存折到银行取钱,也要取 3000.然后银行的系统查询,存
折账户里还有 6000(因为上面钱还没扣),所以它也准备减去 3000,
3)、你的卡里面减去 3000,5000-3000=2000,并且你老婆的存折也是 5000-3000=2000。
4)、结果,你们一共取了 6000,但是卡里还剩下 2000。
下面看程序的模拟过程:
package ;
public class GetMoneyTest {
public static void main(String[] args) {
Account account = new Account(5000);
GetMoneyRun runnable = new GetMoneyRun(account);
new Thread(runnable, "你").start();
new Thread(runnable, "你老婆").start();
}
}
// 账户 Mode
class Account {
private int money;
public Account(int money) {
super();
= money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
= money;
}
}
//runnable 类
class GetMoneyRun implements Runnable {
private Account account;
public GetMoneyRun(Account account) {
= account;
}
@Override
public void run() {
if (() > 3000) {
(().getName() + "的账户有"
+ () + "元");
try {
(10);
} catch (InterruptedException e) {
();
}
int lasetMoney=() - 3000;
(lasetMoney);
(().getName() + "取出来了 3000 元"
+ ().getName() + "的账户还有"
+ () + "元");
} else {
("余额不足 3000" + ().getName()
+ "的账户只有" + () + "元");
}
}
}
多次运行程序,可以看到有多种不同的结果,下面是其中的三种:
你的账户有 5000 元
你老婆的账户有 5000 元
你老婆取出来了 3000 元你老婆的账户还有 2000 元
你取出来了 3000 元你的账户还有-1000 元
你的账户有 5000 元
你老婆的账户有 5000 元
你老婆取出来了 3000 元你老婆的账户还有-1000 元
你取出来了 3000 元你的账户还有-1000 元
你的账户有 5000 元
你老婆的账户有 5000 元
你老婆取出来了 3000 元你老婆的账户还有 2000 元
你取出来了 3000 元你的账户还有 2000 元
可以看到,由于有两个线程同时访问这个 account 对象,导致取钱发生的账户发生问题。当多个
线程访问同一个数据的时候,非常容易引发问题。为了避免这样的事情发生,我们要保证线程同步互
斥,所谓同步互斥就是:并发执行的多个线程在某一时间内只允许一个线程在执行以访问共享数据。
二、同步互斥锁
同步锁的原理:Java 中每个对象都有一个内置同步锁。Java 中可以使用 synchronized 关键字来
取得一个对象的同步锁。synchronized 的使用方式,是在一段代码块中,加上
synchronized(object){ ... }
例如,有一个 show 方法,里面有 synchronized 的代码段:
public void show() {
synchronized(object){
......
}
}
这其中的 object 可以使任何对象,表示当前线程取得该对象的锁。一个对象只有一个锁,所以
其他任何线程都不能访问该对象的所有
synchronized 包括的代码段,直到该线程释放掉这个对象的同步锁(释放锁是指持锁线程退出
了 synchronized 同步方法或代码块)。
注意:synchronized 使用方式有几个要注意的地方(还是以上面的 show 方法举例):
①、取得同步锁的对象为 this,即当前类对象,这是使用的最多的一种方式
public void show() {
synchronized(this){
......
}
}
②、将 synchronized 加到方法上,这叫做同步方法,相当于第一种方式的缩写
public synchronized void show() {
}
③、静态方法的同步
public static synchronized void show() {
}
相当于
public static void show() {
synchronized(当前类名.class)
}
相当于取得类对象的同步锁,注意它和取得一个对象的同步锁不一样
明白了同步锁的原理和 synchronized 关键字的使用,那么解决上面的取钱问题就很简单了,我
们只要对 run 方法里面加上 synchronized 关键字就没有问题了,如下:
@Override
public void run() {
synchronized (account) {
if (() > 3000) {
(().getName() + "的账户有"
+ () + "元");
try {
(10);
} catch (InterruptedException e) {
();
}
int lasetMoney = () - 3000;
(lasetMoney);
(().getName()
+ "取出来了 3000 元" + ().getName()
+ "的账户还有" + () + "元");
} else {
("余额不足 3000"
+ ().getName() + "的账户只有"
+ () + "元");
}
}
}
当甲线程执行 run 方法的时候,它使用 synchronized (account)取得了 account 对象的同步锁,
那么只要它没释放掉这个锁,那么当乙线程执行到 run 方法的时候,它就不能获得继续执行的锁,所
以只能等甲线程执行完,然后释放掉锁,乙线程才能继续执行。
synchronized 关键字使用要注意以下几点:
1)、只能同步方法和代码块,而不能同步变量和类。只要保护好类中数据的安全访问和设置就
可以了,不需要对类使用 synchronized 关键字,所以 Java 不允许这么做。并且想要同步数据,只需
要对成员变量私有化,然后同步方法即可,不需要对成员变量使用 synchronized,java 也禁止这么
做。
2)、每个对象只有一个同步锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对
象上同步?上面的代码中 run 方法使用 synchronized (account)代码块,因为两个线程访问的都是
同一个 Account 对象,所以能够锁定。但是如果是其他的一个无关的对象,就没用了。比如说
synchronized (new Date())代码块,一样没有效果。
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)、如果两个线程要执行一个类中的 synchronized 方法,并且两个线程使用相同的实例来调用
方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一
个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6)、线程睡眠时,它所持的任何同步锁都不会释放。
7)、线程可以获得多个同步锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方
法,则获取了两个对象的同步同步锁。
8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方
法中一部分代码块。
9)、编写线程安全的代码会使系统的总体效率会降低,要适量使用
一个线程取得了同步锁,那么在什么时候才会释放掉呢?
1、同步方法或代码块正常结束
2、使用 return 或 break 终止了执行,或者跑出了未处理的异常。
3、当线程执行同步方法或代码块时,程序执行了同步锁对象的 wait()方法。
三、死锁
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限
期地阻塞,因此程序不能正常运行。简单的说就是:线程死锁时,第一个线程等待第二个线程释放资
源,而同时第二个线程又在等待第一个线程释放资源。这里举一个通俗的例子:如在人行道上两个人
迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,
这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。
“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,
被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加
锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。
一个死锁的造成很简单,比如有两个对象 A 和 B 。第一个线程锁住了 A,然后休眠 1秒,轮到
第二个线程执行,第二个线程锁住了 B,然后也休眠 1秒,然后有轮到第一个线程执行。第一个线程
又企图锁住 B,可是 B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线
程执行。第二个线程又企图锁住 A,可是 A已经被第一个线程锁定了,所以第二个线程也进入阻塞状
态。就这样,死锁造成了。
举个例子:
package ;
public class DeadLock2 {
public static void main(String[] args) {
Object object1=new Object();
Object object2=new Object();
new Thread(new T(object1,object2)).start();
new Thread(new T(object2,object1)).start();
}
}
class T implements Runnable{
private Object object1;
private Object object2;
public T(Object object1,Object object2) {
=object1;
=object2;
}
public void run() {
synchronized (object1) {
try {
(1000);
} catch (InterruptedException e) {
();
}
synchronized (object2) {
("无法执行到这一步");
}
}
};
}
上面的就是个死锁。
第一个线程首先锁住了 object1,然后休眠。接着第二个线程锁住了 object2,然后休眠。在第
一个线程企图在锁住 object2,进入阻塞。然后第二个线程企图在锁住 object1,进入阻塞。死锁了。
四、线程的协调运行
关于线程的协调运行,经典的例子就是生产者和消费者的问题。比如有生产者不断的生产馒头,
放入一个篮子里,而消费者不断的从篮子里拿馒头吃。并且,当篮子满的时候,生产者通知消费者来
吃馒头,并且自己等待不在生产馒头。当篮子没满的的时候,由消费者通知生产者生产馒头。这样不
断的循环。
要完成上面的功能,光靠我们前面的同步等知识,是不能完成的。而是要用到线程间的协调运行。
顶级父类 Object 中有 3种方法来控制线程的协调运行。
notify、notifyAll、wait。其中 wait 有 3 个重载的方法。
这三个方法必须由同步监视器对象(即线程获得的锁对象)来调用,这可分为两种情况:
1、对于使用 synchronized 修饰的同步代码块,因为当前的类对象(this)就是同步监视器,所
以可以再同步方法中直接调用这三个方法。
2、对于使用 synchronized 修饰的同步代码块,同步监视器是 synchronized 后括号的对象,所
以必须使用该对象调用这三个方法。
wait(): 导致当前线程等待,直到其他线程调用该同步监视器的 notify()方法或 notifyAll()
方法来唤醒该线程。wait()方法有三种形式:无时间参数的 wait(一直等待,直到其他线程通知),
带毫秒参数的 wait 和带毫秒、微秒参数的 wait(这两种方法都是等待指定时间后自动苏醒)。调用
wait()方法的当前线程会释放对该同步监视器的锁定。
notify(): 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,
则会选择幻想其中一个线程。选择是任意性。只有当前线程放弃对该同步监视器的锁定后(使用wait()
方法),才可以执行被唤醒的其他线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁
定后,才可以执行被唤醒的线程。
因为使用 wait、notify 和 notifyAll 三个方法一定是在同步代码块中使用的,所以一定要明白
下面几点:
1、如果两个线程是因为都要得到同一个对象的锁,而导致其中一个线程进入阻塞状态。那么只
有等获得锁的线程执行完毕,或者它执行了该锁对象的 wait 方法,阻塞的线程才会有机会得到锁,
继续执行同步代码块。
2、使用 wait 方法进入等待状态的线程,会释放掉锁。并且只有其他线程调用 notify 或者
notifyAll 方法,才会被唤醒。要明白,线程因为锁阻塞和等待是不同的,因为锁进入阻塞状态,会
在其他线程释放锁的时候,得到锁在执行。而等待状态必须要靠别人唤醒,并且唤醒了也不一定会立
刻执行,有可能因为 notifyAll 方法使得很多线程被唤醒,多个线程等待同一个锁,而进入阻塞状态。
还可能是调用 notify 的线程依然没有释放掉锁,只有等他执行完了,其他线程才能去争夺这个锁。
看下面的例子:
package ;
public class ThreadA {
public static void main(String[] args) {
RunnableTest myRunnanle=new RunnableTest();
new Thread(myRunnanle).start();
synchronized (myRunnanle) {
try {
("第一步");
();
} catch (InterruptedException e) {
();
}
("第四步");
}
}
}
class RunnableTest implements Runnable {
public void run() {
try {
(1);
} catch (InterruptedException e) {
();
}
synchronized (this) {
("第二步");
notify();
("第三步");
}
}
}
有两个线程,主线程和我们自己新建的子线程。一步步的分析程序的执行:
1、因为子线程启动后,调用了 sleep,所以主线程先进入同步代码块,而子线程之后因为没有
锁,会进入阻塞状态。
2、主线程的同步代码块执行,打印第一句话,然后调用 wait 方法,进入等待状态。因为进入了
等待状态,所以释放掉了锁,所以子线程可以获得锁,开始执行。
3、子线程执行,打印第二句话,然后调用 notify 方法,将主线程唤醒。可是子线程并没有结束,
依然持有锁,所以主线程不得不进入阻塞状态,等待这个锁。
4、子线程打印第三句话,然后线程正常运行结束,释放掉锁。然后主线程得到了锁,从阻塞进
入运行状态,打印第四句话。
5、完毕
在看一个关于上面提到的生产者和消费者的例子:
首先,是生产物品的 Mode,这里以馒头举例:
// 馒头的实例
class ManTou {
private int id;// 馒头的 id
public ManTou(int id) {
= id;
}
public String toString(){
return "ManTou"+id;
}
}
共享对象,生产者生产的馒头放入其中,消费者从里面拿出馒头,这里以篮子举例:
// 篮子的实例,用来放馒头
class BasketBall {
private int index = 0;// 表示装到第几个了馒头
private ManTou[] manTous = new ManTou[6];// 可以放 6个馒头
// 放进去一个馒头
public synchronized void push(ManTou manTou) {
while(index==){
try {
("篮子满了!");
();
} catch (InterruptedException e) {
();
}
}
(().getName()+"生产
"+());
();
manTous[index] = manTou;
index++;
}
// 拿一个馒头
public synchronized ManTou pop() {
while (index==0) {
try {
("篮子空了!");
();
} catch (InterruptedException e) {
();
}
}
ManTou manTou=manTous[--index];
(().getName()+"吃了
"+());
();
return manTou;
}
}
生产者:
// 生产者,生产馒头
class Producer implements Runnable {
private BasketBall basketBall;
public Producer(BasketBall basketBall) {
= basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou = new ManTou(i);// 生产馒头
(manTou);
try {
(500);
} catch (InterruptedException e) {
();
}
}
}
}
// 消费者,拿馒头吃
class Consumer implements Runnable {
private BasketBall basketBall;
public Consumer(BasketBall basketBall) {
= basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou=();
try {
(1000);
} catch (InterruptedException e) {
();
}
}
}
}
测试:
public class ProducerConsumer {
public static void main(String[] args) {
BasketBall basketBall=new BasketBall();
new Thread(new Producer(basketBall)).start();
new Thread(new Consumer(basketBall)).start();
}
}
Java 多线程(四)、线程池
系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互。在这种情况下,使用
线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用
线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个 Runnable
对象传给线程池,线程池就会启动一条线程来执行该对象的 run 方法,当 run 方法执行结束后,该线
程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个 Runnable 对象的 run 方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,但系统中包含大量并发线程时,
会导致系统性能剧烈下降,甚至导致 JVM 崩溃。而线程池的最大线程数参数可以控制系统中并发的线
程不超过此数目。
在 之前,开发者必须手动的实现自己的线程池,从 之后,Java 内建支持线程池。
与多线程并发的所有支持的类都在 包中。我们可以使用里面的类更加的
控制多线程的执行。
一、Executors 类
中提供 Executors 工厂类来产生连接池,该工厂类中包含如下的几个静态工程
方法来创建连接池:
1、public static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用的、
具有固定线程数的线程池。
2、public static ExecutorService newSingleThreadExecutor():创建一个只有单线程的线程
池,它相当于 newFixedThreadPool 方法是传入的参数为 1
3、public static ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,
系统根据需要创建线程,这些线程将会被缓存在线程池中。
4、public static ScheduledExecutorService newSingleThreadScheduledExecutor:创建只有
一条线程的线程池,他可以在指定延迟后执行线程任务
5、public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):
创建具有指定线程数的线程池,它可以再指定延迟后执行线程任务,corePoolSize 指池中所保存的
线程数,即使线程是空闲的也被保存在线程池内。
上面的几个方法都有一个重载的方法,多传入一个 ThreadFactory 参数的重载方法,使用的比较
少。
二、ExecutorService 类
可以看到上面的 5个方法中,前面 3个方法的返回值都是一个 ExecutorService 对象。该
ExecutorService 对象就代表着一个尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程
任务),程序只要将一个 Runnable 对象或 Callable 对象提交给该线程池即可,该线程就会尽快的执
行该任务。
ExecutorService 有几个重要的方法:
更详细的参考 JDK API 文档。
submit 方法是对 Executor 接口 execute 方法的更好的封装,建议使用 submit 方法。
三、ScheduleExecutorService 类
在上面的 5个方法中,后面 2个方法的返回值都是一个 ScheduleExecutorService 对象。
ScheduleExecutorService 代表可在指定延迟或周期性执行线程任务的线程池。
ScheduleExecutorService 类是 ExecutorService 类的子类。所以,它里面也有直接提交任务的
submit 方法,并且新增了一些延迟任务处理的方法:
下面看看线程池的简单使用:
1、固定大小的线程池:
ackage ;
import ;
import ;
public class PoolTest {
public static void main(String[] args) {
ExecutorService pool=(5);//创建一个固定大小为 5的
线程池
for(int i=0;i<7;i++){
(new MyThread());
}
();
}
}
class MyThread extends Thread{
@Override
public void run() {
(().getName()+"正在执行。。。");
}
}
输出结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-3 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-5 正在执行。。。
pool-1-thread-1 正在执行。。。
可以看到虽然我们呢创建了 7个 MyThread 线程对象,但是由于受线程池的大小限制,只是开启
了 5个线程,这样就减少了并发线程的数量。
2、单任务线程池:
public class PoolTest {
public static void main(String[] args) {
ExecutorService pool=();//创建一个单线程池
for(int i=0;i<7;i++){
(new MyThread());
}
();
}
}
输出结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
可以看到,线程池只开启了一个线程。
3、创建可变尺寸的线程池
public class PoolTest {
public static void main(String[] args) {
ExecutorService pool=();
for(int i=0;i<5;i++){
(new MyThread());
}
();
}
}
看输出结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-3 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-5 正在执行。。。
可以看到,我们没有限制线程池的大小,但是它会根据需求而创建线程。
4、延迟线程池
public class PoolTest {
public static void main(String[] args) {
ScheduledExecutorService pool=(6);
for(int i=0;i<4;i++){
(new MyThread());
}
(new MyThread(), 1000, );
(new MyThread(), 1000, );
();
}
}
输出结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-3 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-6 正在执行。。。
pool-1-thread-1 正在执行。。。
可以明显看到,最后两个线程不是立即执行,而是延迟了 1秒在执行的。
5、单任务延迟线程池
public class PoolTest {
public static void main(String[] args) {
ScheduledExecutorService pool=();
for(int i=0;i<4;i++){
(new MyThread());
}
(new MyThread(), 1000, );
(new MyThread(), 1000, );
();
}
}
上面我们使用的是 JDK 帮我封装好的线程池,我们也可以自己定义线程池,查看源码,我们发现,
Excutors 里面的获得线程的静态方法,内部都是调用 ThreadPoolExecutor 的构造方法。比如:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory
threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, ,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
可以看到,它是通过调用 ThreadPoolExecutor 的构造方法来返回一个线程池的。所以,我们也
可以自己手动的调用 ThreadPoolExecutor 的各种构造方法,来定义自己的线程池规则,不过一般情
况下,使用自带的线程池就够了,不需要自己来实现。
Java 多线程(五)、多线程其他知识简要介绍
一、线程组
/**
* A thread group represents a set of threads. In addition, a thread
* group can also include other thread groups. The thread groups form
* a tree in which every thread group except the initial thread group
* has a parent.
* <p>
* A thread is allowed to access information about its own thread
* group, but not to access information about its thread group's
* parent thread group or any other thread groups.
*
* @author unascribed
* @version , 03/13/08
* @since
*/
一个线程组代表了一系列的线程。并且,一个线程组可以包括其他的线程组。除了初始线程组外,
每个线程组都有一个父线程组,类似于树的结构。
一个线程可以访问它所在线程组的信息, 不可以访问它父线程组和其他线程组的信息。
从这段话中可以大概明白线程组的概念,所有的线程和线程组构成一个树的结构,如下:
查看 Thread 的 API,可以看到,创建一个线程可以指定它的线程组和不指定线程组。如果指定
其所属的线程组,那么该线程组是创建它的线程所属线程组的子线程组。如果不指定线程组,则属于
默认情况,该线程和创建它的线程在同一个线程组。
以上面的图举个简单的例子:
如果 main 线程创建了 Thread1 线程,没有指定 Thread1 所在的线程组,那么 Thread1 就默认和
main 线程属于同一个线程组,即系统线程组。
如果 main 线程创建了 Thread3 线程,没指定 Thread3 所在的线程组为线程组 1,那么线程组 1
就属于系统线程组,和 main 线程在树结构中平级。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行
中途不能改变它所属的线程组。因为指定线程所在线程组是在创建线程的视乎完成的,所以之后不能
再修改它所在的线程组。
下面是 ThreadGroup 的方法摘要
二、线程组与未处理的异常
从 开始,Java 加强了线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,
JVM在结束该线程之前会自动查找是否有对应的 对象,如果找到
该处理器对象,将会调用该对象的 uncaughtException(Thread t,Throwable e)方法来处理该异常。
是 Thread 类的一个内部公共静态接口,该接口内只有一个
方法:
void uncaughtException(Thread t,Throwable t),该方法中的 t代表出现异常的线程,而 e
代表该线程抛出的异常。
Thread 类提供了两个方法来设置异常处理器:
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
线程类的所有线程实例设置默认的异常处理器
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
为指定线程的实例设置异常处理器
ThreadGroup 类实现了 接口,所以每个线程所属的线程组将
会作为默认的异常处理器。当一个线程抛出未处理的异常时,JVM 会首先查找该异常对应的异常处理
器(setUncaughtExceptionHandler 方法设置的异常处理器),如果找到该异常处理器,将调用该异常
处理器处理该异常,否则,JVM 将会调用该线程所属的线程组对象的 uncaughtException 方法来处理
该异常,线程组处理异常的流程如下:
1)、如果该线程组有父线程组,则调用父线程组的 uncaughtException 方法来处理该异常
2)、否则,如果该线程实例所属的线程类有默认的异常处理器(由
setDefaultUncaughtExceptionHandler 方法设置的异常处理器),那么就调用该异常处理器来处理
该异常
3)、否则,将异常调试栈的信息打印到 错误输出流,并结束该线程。
看下面的例子:
class MyHandler implements {
@Override
public void uncaughtException(Thread t, Throwable e) {
("出现了异常");
();
}
}
public class Test{
public static void main(String[] args) {
().setUncaughtExceptionHandler(new MyHandler());
int a=1/0;
}
}
在主线程中设置了异常处理器,最后捕获了异常。