您的位置:首页 > 编程语言 > Java开发

Java-多线程机制详解(二)

2016-10-14 21:42 323 查看

上篇文章多线程机制详解(一) 讲了多线程的相关概念以及创建启动。而这一篇文章将会更加深入去整理多线程机制体系,涉及的运用技巧就是我们项目开发需要使用的,同时重点点出开发项目时候的使用技巧推荐。

文章结构(结合疯狂java讲义总结):1.线程控制;2.线程同步;3.线程通信;4.线程组与线程池(项目运用)

一、线程控制:(其实就是一些工具方法)

(一)join方法定义:Thread类提供的让一个线程等待另一个线程完成的方法(简称就是插队,并不让别人动)。如:主线程中调用了一个子线程,子线程对象调用了join方法,那么主线程必须等子线程执行完了才继续执行自己的代码。(那个时候主线程进入阻塞状态)。重载方式:1. join()–等待被json的线程执行完成;2. join(long millis)–等待被join的线程的时间最长为 millis毫秒,如果在millis毫秒内被join的线程还没执行结束,则不再等待;3. join(long millis,int nanos)–等待被join的线程时间最长为millis毫秒加nanos毫微秒。

(二)后台线程定义:在后台运行,为其他线程提供服务。又称守护线程或精灵线程,如:JVM的垃圾回收线程。使用:调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。注意:要设置为后台线程,必须要在该线程启动之前设置。也就是设置方法必须在start之前。

(三)线程睡眠:sleep 定义:让当前正在执行的宣传暂停一段时间,并进入阻塞状态–常用来暂停程序的执行。做法:调用Thread类的静态sleep方法来实现。sleep重载方式(两个方法受系统计时器和线程调度器的精度和准确度影响):1. static void sleep(long millis) 让当前正在执行的线程暂停millis毫秒,并进入阻塞状态;2. static void sleep(long millis, int nanos)让当前正在执行的线程暂停millis毫秒和nanos毫微秒。

(四)线程让步:yield定义:Thread类提供的静态方法,让当前正在执行的线程暂停,但不会阻塞线程,只是把线程转为就绪状态。做法:调用Thread类的yield方法来实现。注意:当某个线程调用yield方法后,只有当优先级与当前线程相同或者优先级比当前线程更高的处于就绪状态的线程才可获得执行机会。

(五)改变线程优先级: 定义:优先级高的线程获得较多执行机会,而优先级低的线程则获得较少的执行机会(每个线程默认的优先级与创建它的父进程优先级相同)。做法: Thread类提供的setProirity(int newPriority), getPriority方法来设置和返回指定线程的优先级。注意:不同平台提供的优先级不一样。为了更好的移植性,推荐使用三个静态常量来设置(MAX_PROIRITY 其值为10、MIN_PROIRITY 其值为1、NORM_PROIRITY 其值为5)。

补充:sleep和yield的区别

1. sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但是!!yield方法只会给优先级相同或者优先级更高的线程执行机会

2.sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;但!!yield不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态(因此有可能某个线程调用yield方法暂停后,立即在此获得处理器资源而被执行)。

3.sleep方法声明抛出InterruptedException异常,所以调用sleep方法时,要么捕捉该异常,要么显示声明抛出异常;而yield没有声明抛出任何异常

4. sleep方法比yield有更好的移植性。通常不建议用yield来控制并发线程

二、线程同步(当使用多个线程访问同一个数据时,容易出现线程安全问题):

线程同步体系有如下几点:(1).线程安全问题和线程安全类;(2).同步代码块和同步方法(重点);(3)同步锁(重点);(4)死锁。

(1).线程安全问题和线程安全类:

出现线程安全原因:run方法的方法体不具有同步安全性。(比如:程序两个并发线程在修改Account账号对象–取钱的时候,而系统恰好在取钱的时候执行线程切换,切换给另一个修改Account账号对象去取钱。那么就容易出问题了。)

线程安全类特征:1.该类的对象可被多个线程安全访问;2.每个线程调用该对象的任意方法之后都会得到正确结果;3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

(2).同步代码块和同步方法:

1.同步代码块:作用:解决两个进程并发修改同一个文件可能造成异常的问题。

synchronized(obj){

//此处代码为同步代码块
}


obj是同步监视器。目的:阻止两个线程对同一个共享资源进行并发访问。使用注意:1.线程开始执行同步代码块前,必须先获得对同步监视器的锁定;2.任何时刻只能有一个线程可获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。推荐:使用可能被并发访问的共享资源充当同步监视器。

2.同步方法做法:使用synchronized关键字来修饰某个方法。注意:1.对synchronized修饰的实例方法(非static方法),无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象;2.synchronized可修饰方法、代码块,但是不能修饰构造器、成员变量。

释放同步监视器的锁定情况:



线程不释放同步监视器的情况:



补充:同步代码块跟同步方法使用与竞争资源相关的隐式的同步监视器,并强制要求加锁和释放锁要出现在一个块结构中。而且当获得多个锁时,它们必须以相反的顺序释放,且必须与所有锁被获取时相同的范围内释放所有锁

(3)同步锁(重点):

同步锁定义:通过显示定义同步锁对象来实现同步的机制(同步锁由Lock对象来充当)。

Lock对象定义:Lock是控制多个线程对共享资源进行访问的工具(锁提供对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前需先获得Lock对象)。优点:比synchronized有更广泛的锁定操作,允许实现更灵活的结构,可具有差别很大的属性,支持多个相关的Condition对象。对比同步代码块和同步方法:(重点!!!!!) 1.与使用同步方法相似,但使用Lock时显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器;2.使用Lock对象时每个Lock对象对应一个Account对象,一样可保证对于同一个Account对象。同一时刻只能有一个线程进入临界区。

//使用lock对象来锁住此类,使得此类线程安全
public class Account
{
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
//无参
public Account(){}
// 有参构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// 此处省略了accountNo和balance的setter和getter方法

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}

// balance的setter和getter方法
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}


补充:新特性以及推荐:

Java5提供的两个根接口:



推荐:在实现线程安全的控制中,常用ReentrantLock(可重入锁)

(4)死锁:

定义:两个线程相互等待对方释放同步监视器时,就胡发生死锁

后果:不会发生异常,不会给出提示,只是所有线程处于阻塞状态,无法继续。

三、线程通信:

线程通信有三大方式:(1)传统线程通信;(2)使用Condition控制线程通信;(3)使用阻塞队列(BlockingQueue)控制线程通信。

(1)传统线程通信:

方法:借助Object类提供的wait、notify、notifyAll三个方法。这三个方法不属于Thread类,且三个方法必须由同步监视器对象来调用。情况分析:1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可在同步方法中直接调用这三个方法;2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用三个方法。

传统线程通信三大方法详解:

wait:作用:导致当前线程等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。(调用wait方法的当前线程会释放对该同步监视器的锁定)。使用形式:

notify:作用:唤醒在此同步监视器上等待的单个线程。注意:如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。注意:只有当前线程放弃对该同步监视器的锁定后(使用wait方法),才可执行被唤醒的线程

notifyAll:作用:唤醒在此同步监视器上等待的所有线程。注意:注意:只有当前线程放弃对该同步监视器的锁定后,才可执行被唤醒的线程。

(2)使用Condition控制线程通信:

定义:直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能用wait、notify、notifyAll三个方法了,就是显示使用Lock对象来充当同步监视器,而使用Condition对象来暂停、唤醒指定线程的做法。Condition作用:1.可让那些得到Lock对象却无法继续执行的线程释放Lock对象;2.可唤醒其他处于等待的线程。细节注意:1.Condition将同步监视器方法(wait等等)分解成不同的对象,通过这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set),这种情况下,Lcok替代了同步方法或同步代码块,Condition替代了同步监视器;2.Condition实例被绑定在一个Lock对象上。

三大方法详解: 1.await作用:导致当前线程等待,直到其他线程调用该Condition的signal方法或signalAll方法来唤醒。注意:有许多使用形式。2.signal作用:唤醒在此Lock对象等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。注意:只有当前线程放弃对该同步监视器的锁定后(使用await方法),才可执行被唤醒的线程。3.sinalAll作用:换U型在此Lock对象上等待的所有线程。注意:只有当前线程放弃对该Lock的锁定后,才可执行被唤醒的线程。

(3)使用阻塞队列(BlockingQueue)控制线程通信:

定义:BlockingQueue是Queue的子接口,作为线程同步的工具。特征:当生产者线程视图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程视图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。

BlockingQueue提供的方法详解:1.put(E e)。尝试把E元素放入BlockingQueue中,如果队列元素已满,则阻塞该线程。 2.take()。尝试从BlockingQueue的头部取出元素,如果队列的元素已空,则阻塞该线程。

也可用Queue接口提供的方法:

1.在队列尾部插入元素:有add(E e),offer(E e)和put(E e),当队列满的时候,三个方法分别会抛出异常、返回false、阻塞队列。

2.在队列头部删除并返回删除的元素:有remove()、poll()和take(),当队列已空时,三个方法分别抛出异常、返回false、阻塞队列。

3.在队列头部取出但不删除元素:有element()、peek(),当队列已空,这两个方法分别抛出异常、返回false。

import java.util.concurrent.*;

class Producer extends Thread{
private BlockingQueue <String> bq;
public Producer(BlockingQueue<String> bq){
this.bq=bq;
}
public void run(){
String [] strArr=new String[]{
"JAVA",
"Struts",
"Spring"
};
for(int i=0;i<20;i++){
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
// 尝试放入元素,如果队列已满,线程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}

}
}
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消费者准备消费集合元素!");
try
{
Thread.sleep(200);
// 尝试取出元素,如果队列已空,线程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}

public class BlockingQueue1 {

public static void main(String[] args) {
// TODO Auto-generated method stub

// 创建一个容量为1的BlockingQueue
BlockingQueue<String>bq=new ArrayBlockingQueue<>(1);
// 启动3条生产者线程,三大生产线程抢占生产。
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一条消费者线程
new Consumer(bq).start();
}

}


四、线程组与线程池:

(1)线程组(ThreadGroup)

[b]定义:表示线程组,可对一批线程进行分类管理。[/b]

[b]注意:1.默认情况下,子线程和创建它的父线程处于同意线程组;2.一旦某线程加入到指定线程组后,该线程就一直属于该线程组了,直到该线程死亡。[/b]

[b]设置:新线程属于哪个线程组:1.Thread(ThreadGroup group,Runnable target)—-以target的run方法作为线程执行体创建新线程,属于group线程组。 2.Thread(ThreadGroup group,Runnable target,String name)—以target的run方法作为线程执行体创建新线程,该线程属于group线程组,线程名为name。 3.Thread(ThreadGroup group,String name)—创建新线程,名为name,属于group线程组。[/b]

[b]方法接口与构造器:[/b]

1.ThreadGroup构造器:ThreadGroup(String name)和ThreadGroup(ThreadGroup parnet,String name)–name为线程组名,名字可通过getName方法获取,但不允许改变线程组名。

2.void uncaughtException(Thread t,Throwable e)–可处理线程组内的任意线程所抛出的未处理异常。线程组处理异常的流程:1.如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常;2.如果该线程实例所属的线程类有默认的异常处理器(setDefaultUncaughtExceptionHandler()方法设置的异常处理器),那么就调用该异常处理器来处理;3.如果该异常对象是ThreadDeath的对象,则不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。

[b]注意:ThreadGroup实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认异常处理器[/b]

ThradGroup提供方法操作线程组的线程:

int activeCount()–返回此线程组中活动线程的数目

interrupt–中断此线程组的所有线程

isDaemon–判断该线程组是否是后台线程组

setDaemon(boolean daemon)–把线程组设置成后台线程组。注意:当后台线程组的最后一个线程执行结束,后台线程组将自动销毁。

setMaxPriority–设置线程组的最高优先级

class MyThread extends Thread
{
// 提供指定线程名的构造器
public MyThread(String name)
{
super(name);
}
// 提供指定线程名、线程组的构造器
public MyThread(ThreadGroup group , String name)
{
super(group, name);
}
public void run()
{
for (int i = 0; i < 20 ; i++ )
{
System.out.println(getName() + " 线程的i变量" + i);
}
}
}
public class ThreadGroupTest
{
public static void main(String[] args)
{
// 获取主线程所在的线程组,这是所有线程默认的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名字:"
+ mainGroup.getName());
System.out.println("主线程组是否是后台线程组:"
+ mainGroup.isDaemon());
new MyThread("主线程组的线程").start();
ThreadGroup tg = new ThreadGroup("新线程组");
tg.setDaemon(true);
System.out.println("tg线程组是否是后台线程组:"
+ tg.isDaemon());
MyThread tt = new MyThread(tg , "tg组的线程甲");
tt.start();
new MyThread(tg , "tg组的线程乙").start();
}
}


(2)线程池

[b]原因:启动一个新线程成本高,尤其是当程序需要大量生存期短的线程就更需要线程池。[/b]

[b]做法:程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run或call方法,当run或call方法执行完后,该线程不会死亡,而是再次返回线程池成为空闲状态。而且还可以控制系统中并发线程的数量。[/b]

[b]线程池相关类:[/b]

Executor工厂类的静态工厂方法产生线程池:



其中前三个方法返回一个ExecutorService对象,对象代表一个线程池。中间两个方法返回一个ScheduledExecutorService线程池,属于ExecutorService的子类,可在指定延迟后执行线程任务。最后两个方法:Java8新增,可利用多CPU并行能力。两个方法生产work stealing池,相当于后台线程,如果前台线程都死亡,则work stealing池也自动死亡。

ExecutorService代表尽快执行线程的线程池。

ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。提供了四个方法。

注意:一、shutdown方法–用完一个线程池后,应该调用该线程池的shutdown方法,启动线程池的关闭序列,线程池不再接收新任务,但会先将以前所有已提交的线程池任务执行完成。二、shutdownNow()方法–关闭线程池,方法停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

使用线程池来执行线程任务的步骤

一、调用Executors类的静态工厂方法创建一个ExecutorService对象

二、创建Runnable实现类或Callable实现类的实例,作为线程执行任务

三、调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例

四、当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

//线程池使用范例--取自疯狂java讲义
public class ThreadPoolTest
{
public static void main(String[] args)
throws Exception
{
// 创建足够的线程来支持4个CPU并行的线程池
// 创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
// 使用Lambda表达式创建Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ "的i值为:" + i);
}
};
// 向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
// 关闭线程池
pool.shutdown();
}
}


好了,Java-多线程机制详解(二)讲完了。欢迎在下面指出错误,共同学习!

转载请注明:【JackFrost的博客】

更多内容,可以访问JackFrost的博客

本博主最近会一直更新Java知识、数据结构与算法、设计模式的文章(因为最近在学习,哈哈哈),欢迎关注。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: