Java-多线程第三篇3种创建的线程方式、线程的生命周期、线程控制、线程同步、线程通信
1、Java使用Thread类代表线程。
所有的线程对象必须是Thread类或其子类的实例。
当线程继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName()方法返回当前线程的名字,因此可以直接调用getName()方法返回当前线程的名字。
Thread.currentThread():该方法总是返回当前正在执行的线程对象。
2、创建线程方式1:继承Thread类创建线程类
这种方式创建线程的步骤一般为:
1》定义Thread类的子类,并重写该类的run()方法,该方法作为线程的线程执行体。
2》创建Thread子类的实例,即线程对象。
3》调用线程对象的start()方法启动线程。
举个例子:
public class extendsThread extends Thread{ ... @Override public void run(){ ... //do something
System.out.println(this.getName()); } public static void main(String[] agrs){ //创建并启动第一个线程 new extendsThread().start(); //创建并启动第二个线程 new extendsThread().start(); } }
3、创建线程方式2:实现Runnable接口
这种方式创建线程的步骤一般为:
1》定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2》创建Runnable接口实现类的实例,并将该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。本质是Thread对象负责执行实现类对象的run()方法体。
3》调用线程对象的start()方法来启动该线程。
举个例子:
public class ImplRunnable implements Runnable{ ... @Override public void run(){ ... //do something
System.out.println(Thread.currentThread().getName());
} public static void main(String agrs){ ... System.out.println(Thread.currentThread().getName()); ImplRunnable target=new ImplRunnable(); //创建并启动第一个线程 new Thread(target,"Thread1").start(); //创建并启动第二个线程 new Thread(target,"Thread2").start(); } }
4、使用Callable和Future创建线程:创建有返回值的线程
这种方式创建线程的步骤一般为:
1》创建Callable接口的实现类,并实现call()方法,该方法作为线程执行体,且该方法有返回值。然后创建该实现类的实例。
2》使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3》使用FutureTask对象作为Thread对象的target创建并启动新线程。
4》调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
举个例子:
public class ImplCallable implements Callable<Integer>{ ... @Override public Integer call(){ ... //do something System.out.println(Thread.currentThread().getName()); //return a value. return 1; } public static void main(String[] agrs) throws InterruptedException, ExecutionException{ ... //创建并启动一个线程 FutureTask<Integer> target=new FutureTask<Integer>(new ImplCallable()); new Thread(target,"Thread3 with value.").start(); //获取返回值 System.out.println(target.get()); } }
5、线程的生命周期
线程进入阻塞状态:
线程的死亡:
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来之后,它就拥有和主线程相同的地位,它不会受主线程的影响。可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡两种状态时,该方法返回false。不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡的线程不能再次作为线程执行。
6、控制线程
1》join():线程。Thread的静态方法。
A线程调用B线程的join()方法,A线程将被阻塞,直至B线程执行结束。join()方法有如下三种重载方式:
1>join():等待被join的线程执行完毕。
2>join(long millis):等待被join的线程的事时间最长为millis毫秒,如果millis毫秒被join的线程依然没有结束,则不再等待。
3>join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫秒。(一般计算机硬件、操作系统本身无法做到这么精确)。
2》setDaemon(true):后台线程、守护线程、精灵线程
如果所有的前台线程都死亡了,后台线程也会自动死亡。调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。
Thread还提供了isDaemon()方法,用于判断指定线程是否为后台线程。
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
3》sleep():让线程睡眠。Thread的静态方法。
在指定的睡眠时间内,即使系统中没有其他线程可执行,处于sleep()中的线程也不会执行。sleep()方法通常用来暂停线程。
4》yield():线程让步。Thread的静态方法。可以让线程暂停,但不会阻塞线程,线程会进入就绪状态,有可能立即又得到执行。当线程执行yield()方法后,优先级与之相等或者比它高的线程才会得到执行的机会。
比较:
5》改变线程的优先级
每个线程默认的优先级与创建它的父线程的优先级相同。默认情况下,main线程具有普通优先级。
Thread提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority(int newPriority)方法的参数可以是一个整数,范围1~10,值越大优先级越高。Thread也提供了三个静态常量设置的值:
7、线程同步:方式(1)synchronized的同步代码块、同步方法;方式(2)同步锁(Lock或ReadWriteLock接口的实现类)。
1》同步代码块:
//其中的obj就是同步监视器。 //线程在执行到下面的代码块的时候,必须先获得对同步监视器的锁定。
//任何时刻只能有一个线程可以获得同步监视器的锁定,同步代码块执
//行结束,同步监视器自动被释放锁定。 sychronized(obj){ ... }
2》同步方法:
使用sychronized修饰的某个方法(可能是实例方法,也可能是静态方法)。
1>修饰实例方法:无需显式指定同步监视器,同步方法的同步监视器就是this,即调用该方法的实例对象。
2>修饰静态方法:。。。
==》注意:sychronized可以修饰代码块、成员方法。但是不能修饰构造器、成员变量等。
==》为了减少线程安全问题带来的性能影响:
1>不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步(可以参考哪些资源会被改变)。
2>如果可变类有两种环境:单线程、多线程环境。可以考虑对该类提供两个版本,在不同的环境中使用不同的版本。
(典型单线程、多线程线程安全版本:StringBuilder(非线程安全版本)是单线程环境下使用;StringBuffer(线程安全版本)在多线程环境下使用)
释放同步监视器的锁定,这个不能显式地控制,一般的时机为:
可大概总结为线程要从同步代码块、同步方法的代码块中退出(被迫或正常执行结束),这时候就会自动释放同步监视器。
3》同步锁(Lock):
Lock对象充当同步对象。
1)Java 5提供了Lock、ReadWriteLock两个根接口,并为:
1>Lock==>ReetrantLock(可重入锁)实现类:
2>ReadWriteLock==>ReetrantReadWriteLock(可重入读写锁)实现类:提供了Writing、ReadingOptimistic、Reading三种锁模式。
可重入锁,指的是一个线程可以对已被加锁的ReetrantLock / ReetrantReadWriteLock对象再次加锁,ReetrantLock / ReetrantReadWriteLock对象会维持一个计数器来追踪lock()方法的嵌套调用。线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
2)Java 8新增StampedLock类,在大多数场景中可以替代ReetrantReadWriteLock(可重入读写锁)。
常用的锁是ReetrantLock(可重入锁),其一般使用格式:
public class A{ //定义锁对象 private final ReentrantLock lock=new ReentrantLock(); ... public void method(){ //加锁对象 lock.lock(); try{ //需要保证线程同步的代码 ... } finally{ //释放锁 lock.unlock(); } } }
总结:同步代码块、同步方法使用的是与资源竞争相关的、隐式的同步监视器,且强制要求加锁和锁释放要出现在一个块结构中,而且获得了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
==》死锁:
两个线程相互等待对方释放同步监视器时就会发生死锁,简单的例子A等待B的筷子吃面然后才能空出盘子,但B在等待A的盘子盛面吃了才可以给A筷子,相互之间等待需要的资源,就形成了死锁。出现了死锁,所有线程处于阻塞态,无法继续。
8、线程通信
1》传统的线程通信
Object类提供了wait()、notify()、notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类。且三个方法必须由同步监视器对象来调用,可分为两种情况:
关于上面的wait()、notify()、notifyAll()三个方法:
使用举例:
public class A{ ... public sychronized void getMoney(){ try{ if(!flag){ wait(); } else{ ... flag=false; notifyAll(); } } catch{ ... } } public sychronized void setMoney(){ try{ if(!flag){ wait(); } else{ ... flag=true; notifyAll(); } } catch{ ... } } }
2》使用Condition控制线程通信
这种情况针对不使用sychronized关键字来保证同步(即不是同步代码块或者同步方法的方式),而是使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用上面的三个方法(wait()、notify()、notifyAll())进行线程间通信了。
针对使用Lock对象保证同步的情况,Java提供了Condition类来保持协调。
使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程。
使用举例:
public class A{ //定义锁对象 private final Lock lock=new ReentrantLock(); //指定Lock对象的对应的Condition private final Condition cond=lock.new Condition(); ... public void getMoney(){ //加锁 lock.lock(); try{ if(!flag){ cond.await(); } else{ ...
flag=false; cond.signalAll(); } } catch{ ... } finally{ //释放锁 lock.unlock(); } } public void setMoney(){ //加锁 lock.lock(); try{ if(!flag){ cond.await(); } else{ ...
flag=true; cond.signalAll(); } } catch{ ... } finally{ //释放锁 lock.unlock(); } } }
3》使用阻塞队列(BlockingQueue)控制线程通信
Java 5提供了一个BlockingQueue接口,Queue的子接口,主要用作线程同步工具。特征:当生产者试图往BlockingQueue放入元素时,如果队列已满,则线程被阻塞;如果消费者线程试图从BlockingQueue取元素时,如果队列已空,则线程被阻塞。
。。。详细使用参考相关的API。
9、线程组合未处理的异常