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) { System.out.println(Thread.currentThread().getName()); MyThread myThread=new MyThread(); myThread.start(); } } class MyThread extends Thread{ int i=0; @Override public void run() { while (i<10) { System.out.println(this.getName()+" i的值 "+i); i++; } } } |
2、实现Runnable接口
public class Test1 { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); Thread thread=new Thread(new MyRunnable()); thread.start(); } } class MyRunnable implements Runnable{ int i=0; @Override public void run() { while (i<10) { System.out.println(Thread.currentThread().getName()+" i的值 "+i); i++; } } } |
注意:
①、在继承Thread的方式中,可以使用getName()方法,来获得当前线程的名字,这是因为在Thread类中,有这个方法。可是在实现Runnable方式中,却不可以使用this.getName(),因为Runnable接口没有这个方法(可以看出来,因为我们没有提示我们需要重写这个方法),所以只能通过Thread的静态方法Thread.currentThread()取得当前的Thread对象,在调用getName()方法,来取得当前线程的名字。
②、对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。只有调用start()方法才会启动新线程。
3、两种方式的对比
采用实现Runnable接口方式的多线程具有优势,一般都会使用这种方式:
1、线程类只是实现了Runnable接口,还可以继承其他类。
2、在这种方式下,可以多个线程共享一个Runnable target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
Java多线程(二)、线程的生命周期和状态控制
一、线程的生命周期
线程状态转换图:
1、新建状态
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。
2、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
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.lang.IllegalThreadStateException异常。
二、线程状态的控制
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++){ System.out.println("main"+i); Thread.sleep(100); } } } |
可以明显看到打印的数字在时间上有些许的间隔。
注意如下几点问题
①、sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。看下面的例子:
public class Test1 { public static void main(String[] args) throws InterruptedException { System.out.println(Thread.currentThread().getName()); MyThread myThread=new MyThread(); myThread.start(); myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程 Thread.sleep(10); for(int i=0;i<100;i++){ System.out.println("main"+i); } } } |
②、Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.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++) { System.out.println(this.getName()+"线程" + i + "次执行!"); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
看某一次的运行结果:
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);// 设置线程的名称 this.setPriority(pro);// 设置优先级 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } } } |
关于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(); thread.start(); thread.join(1);//将主线程加入到子线程后面,
不过如果子线程在1毫秒时间内没执行完,
则主线程便不再等待它执行完,进入就绪状态,等待cpu调度 for(int i=0;i<30;i++){ System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!"); } } } class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } } |
在这个例子中,在主线程中调用thread.join(); 就是将主线程加入到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++) { System.out.println(this.getName() + "线程第" + 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()); t2.setDaemon(true); //设置为守护线程 t2.start(); t1.start(); } } class MyCommon extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyDaemon implements Runnable { public void run() { for (long i = 0; i < 9999999L; i++) { System.out.println("后台线程第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
执行结果:
后台线程第0次执行! 线程1第0次执行! 线程1第1次执行! 后台线程第1次执行! 后台线程第2次执行! 线程1第2次执行! 线程1第3次执行! 后台线程第3次执行! 线程1第4次执行! 后台线程第4次执行! 后台线程第5次执行! 后台线程第6次执行! 后台线程第7次执行! |
从上面的执行结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。
守护线程的用途:
守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。
守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
6、如何结束一个线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法。
1、正常执行完run方法,然后结束掉
2、控制循环条件和判断条件的标识符来结束掉线程
比如说run方法这样写:
class MyThread extends Thread { int i=0; @Override public void run() { while (true) { if(i==10) break; i++; System.out.println(i); } } } |
或者
class MyThread extends Thread { int i=0; boolean next=true; @Override public void run() { while (next) { if(i==10) next=false; i++; System.out.println(i); } } } |
或者
class MyThread extends Thread { int i=0; @Override public void run() { while (true) { if(i==10) return; i++; System.out.println(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(); thread.start(); } } class MyThread extends Thread { int i=1; @Override public void run() { while (true) { System.out.println(i); System.out.println(this.isInterrupted()); try { System.out.println("我马上去sleep了"); Thread.sleep(2000); this.interrupt(); } catch (InterruptedException e) { System.out.println("异常捕获了"+this.isInterrupted()); 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(); thread.start(); Thread.sleep(3000); thread.interrupt(); } } class MyThread extends Thread { int i=0; @Override public void run() { while (true) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("中断异常被捕获了"); return; } i++; } } } |
多测试几次,会发现一般有两种执行结果:
或者
这两种结果恰恰说明了 只要一个线程的中断状态一旦为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 com.tao.test; 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(); this.money = money; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } } //runnable类 class GetMoneyRun implements Runnable { private Account account; public GetMoneyRun(Account account) { this.account = account; } @Override public void run() { if (account.getMoney() > 3000) { System.out.println(Thread.currentThread().getName() + "的账户有" + account.getMoney() + "元"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } int lasetMoney=account.getMoney() - 3000; account.setMoney(lasetMoney); System.out.println(Thread.currentThread().getName() + "取出来了3000元" + Thread.currentThread().getName() + "的账户还有" + account.getMoney() + "元"); } else { System.out.println("余额不足3000" + Thread.currentThread().getName() + "的账户只有" + account.getMoney() + "元"); } } } |
多次运行程序,可以看到有多种不同的结果,下面是其中的三种:
你的账户有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 (account.getMoney() > 3000) { System.out.println(Thread.currentThread().getName() + "的账户有" + account.getMoney() + "元"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } int lasetMoney = account.getMoney() - 3000; account.setMoney(lasetMoney); System.out.println(Thread.currentThread().getName() + "取出来了3000元" + Thread.currentThread().getName() + "的账户还有" + account.getMoney() + "元"); } else { System.out.println("余额不足3000" + Thread.currentThread().getName() + "的账户只有" + account.getMoney() + "元"); } } } |
当甲线程执行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 com.tao.test; 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) { this.object1=object1; this.object2=object2; } public void run() { synchronized (object1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object2) { System.out.println("无法执行到这一步"); } } }; } |
上面的就是个死锁。
第一个线程首先锁住了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 com.tao.test; public class ThreadA { public static void main(String[] args) { RunnableTest myRunnanle=new RunnableTest(); new Thread(myRunnanle).start(); synchronized (myRunnanle) { try { System.out.println("第一步"); myRunnanle.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("第四步"); } } } class RunnableTest implements Runnable { public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { System.out.println("第二步"); notify(); System.out.println("第三步"); } } } |
有两个线程,主线程和我们自己新建的子线程。一步步的分析程序的执行:
1、因为子线程启动后,调用了sleep,所以主线程先进入同步代码块,而子线程之后因为没有锁,会进入阻塞状态。
2、主线程的同步代码块执行,打印第一句话,然后调用wait方法,进入等待状态。因为进入了等待状态,所以释放掉了锁,所以子线程可以获得锁,开始执行。
3、子线程执行,打印第二句话,然后调用notify方法,将主线程唤醒。可是子线程并没有结束,依然持有锁,所以主线程不得不进入阻塞状态,等待这个锁。
4、子线程打印第三句话,然后线程正常运行结束,释放掉锁。然后主线程得到了锁,从阻塞进入运行状态,打印第四句话。
5、完毕
在看一个关于上面提到的生产者和消费者的例子:
首先,是生产物品的Mode,这里以馒头举例:
// 馒头的实例 class ManTou { private int id;// 馒头的id public ManTou(int id) { this.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==manTous.length){ try { System.out.println("篮子满了!"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"生产"+manTou.toString()); this.notify(); manTous[index] = manTou; index++; } // 拿一个馒头 public synchronized ManTou pop() { while (index==0) { try { System.out.println("篮子空了!"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } ManTou manTou=manTous[--index]; System.out.println(Thread.currentThread().getName()+"吃了"+manTou.toString()); this.notify(); return manTou; } } |
生产者:
// 生产者,生产馒头 class Producer implements Runnable { private BasketBall basketBall; public Producer(BasketBall basketBall) { this.basketBall = basketBall; } @Override public void run() { for (int i = 0; i < 20; i++) { ManTou manTou = new ManTou(i);// 生产馒头 basketBall.push(manTou); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
// 消费者,拿馒头吃 class Consumer implements Runnable { private BasketBall basketBall; public Consumer(BasketBall basketBall) { this.basketBall = basketBall; } @Override public void run() { for (int i = 0; i < 20; i++) { ManTou manTou=basketBall.pop(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
测试:
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崩溃。而线程池的最大线程数参数可以控制系统中并发的线程不超过此数目。
在JDK1.5之前,开发者必须手动的实现自己的线程池,从JDK1.5之后,Java内建支持线程池。
与多线程并发的所有支持的类都在java.lang.concurrent包中。我们可以使用里面的类更加的控制多线程的执行。
一、Executors类
JDK1.5中提供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 com.tao.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class PoolTest { public static void main(String[] args) { ExecutorService pool=Executors.newFixedThreadPool(5);//创建一个固定大小为5的线程池 for(int i=0;i<7;i++){ pool.submit(new MyThread()); } pool.shutdown(); } } class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().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=Executors.newSingleThreadExecutor();//创建一个单线程池 for(int i=0;i<7;i++){ pool.submit(new MyThread()); } pool.shutdown(); } } |
输出结果:
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=Executors.newCachedThreadPool(); for(int i=0;i<5;i++){ pool.submit(new MyThread()); } pool.shutdown(); } } |
看输出结果:
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=Executors.newScheduledThreadPool(6); for(int i=0;i<4;i++){ pool.submit(new MyThread()); } pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS); pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS); pool.shutdown(); } } |
输出结果:
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=Executors.newSingleThreadScheduledExecutor(); for(int i=0;i<4;i++){ pool.submit(new MyThread()); } pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS); pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS); pool.shutdown(); } } |
上面我们使用的是JDK帮我封装好的线程池,我们也可以自己定义线程池,查看源码,我们发现,Excutors里面的获得线程的静态方法,内部都是调用ThreadPoolExecutor的构造方法。比如:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 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 1.66, 03/13/08 * @since JDK1.0 */ |
一个线程组代表了一系列的线程。并且,一个线程组可以包括其他的线程组。除了初始线程组外,每个线程组都有一个父线程组,类似于树的结构。
一个线程可以访问它所在线程组的信息, 不可以访问它父线程组和其他线程组的信息。
从这段话中可以大概明白线程组的概念,所有的线程和线程组构成一个树的结构,如下:
查看Thread的API,可以看到,创建一个线程可以指定它的线程组和不指定线程组。如果指定其所属的线程组,那么该线程组是创建它的线程所属线程组的子线程组。如果不指定线程组,则属于默认情况,该线程和创建它的线程在同一个线程组。
以上面的图举个简单的例子:
如果main线程创建了Thread1线程,没有指定Thread1所在的线程组,那么Thread1就默认和main线程属于同一个线程组,即系统线程组。
如果main线程创建了Thread3线程,没指定Thread3所在的线程组为线程组1,那么线程组1就属于系统线程组,和main线程在树结构中平级。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。因为指定线程所在线程组是在创建线程的视乎完成的,所以之后不能再修改它所在的线程组。
下面是ThreadGroup的方法摘要
二、线程组与未处理的异常
从JDK1.5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,将会调用该对象的uncaughtException(Thread
t,Throwable e)方法来处理该异常。
Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口内只有一个方法:
void uncaughtException(Thread t,Throwable
t),该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。
Thread类提供了两个方法来设置异常处理器:
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) |
线程类的所有线程实例设置默认的异常处理器
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) |
为指定线程的实例设置异常处理器
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理的异常时,JVM会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler方法设置的异常处理器),如果找到该异常处理器,将调用该异常处理器处理该异常,否则,JVM将会调用该线程所属的线程组对象的uncaughtException方法来处理该异常,线程组处理异常的流程如下:
1)、如果该线程组有父线程组,则调用父线程组的uncaughtException方法来处理该异常
2)、否则,如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器),那么就调用该异常处理器来处理该异常
3)、否则,将异常调试栈的信息打印到System.err错误输出流,并结束该线程。
看下面的例子:
class MyHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("出现了异常"); e.printStackTrace(); } } public class Test{ public static void main(String[] args) { Thread.currentThread().setUncaughtExceptionHandler(new MyHandler()); int a=1/0; } } |
在主线程中设置了异常处理器,最后捕获了异常。
|