控制线程:
join线程:
Thread提供了另一个线程完成的办法:join()办法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直接被join方法加入的join线程完成为止。
Join()方法通常由使用线程的程序调用,以将大问题化成许多小问题,每个问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。
public class JoinThread extends Thread{
//提供了一个有参数构造器,用于设置该线程的名字
public JoinThread(String name) {
super(name);
}
//重写Run方法
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+" “+i);
}
}
public static void main(String[] args) throws Exception {
//启动子进程
new JoinThread(“新线程”).start();
for (int i = 0; i < 100; i++) {
if(i==20)
{
JoinThread jt=new JoinThread(“被Join的新线程”);
jt.start();
//main线程调用了jt线程的join方法,main线程
//必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+” “+i );
}
}
}
Join方法有三种重载的形式:
Join():等待被join的线程执行完成
Join(long millis):等待被join的线程的时间最长为millis毫秒,如果在millis毫秒内,被join的线程还没有执行结束则不再等待
Join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒,加上nanos微秒(千分之一毫秒)
后台线程:
有一种线程,它是在后台运行的,他的任务是为其他线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果前台线程都死亡,后台线程会自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。如:
public class InvokeRun extends Thread {
private int i;
//重写run方法,run方法的方法体就是线程的执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName()+” “+i);
}
}
public static void main(String[] args) {
InvokeRun t=new InvokeRun();
//将此线程设置成后线程
t.setDaemon(true);
//启动后台进程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+” "+i);
}
}
}
Thread类还提供了一个isDaemon()方法,用于判断指定线程是否后台线程
主线程默认是前台线程。并不是所有的线程默认是前台线程,有些线程默认是后台线程:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令,到它做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动前设置,也就是说setDaemon(true):必须在Start()方法之前调用,否则将会引发IllegalThreadStateException异常。
线程睡眠:sleep
Sleep有两种重载的形式:
1. static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器的精度和准确读的影响。
2. Static void sleep(long millis, int nanos):让当前正在执行的线程暂时millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到系统计时器和线程的调度器的精度和准确度的影响。
Sleep方法常用暂停程序的执行。
public class TestSleep {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(“当前时间:”+new Date());
//调用sleep方法暂停1s
Thread.sleep(1000);
}
}
}
线程让步(yield):
Yield()方法可以让当前正在执行的线程暂停,但他不会阻塞该线程,他只是将该线程转入就绪状态。Yield让当前正在执行的线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才会获得执行机会,如:
public class TestYield extends Thread {
public TestYield() {}
public TestYield(String name)
{
super(name);
}
//定义run方法作为线程的执行体
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+" “+i);
//当i等于20时,使用yield方法让当前线程让步
if(i20)
{
Thread.yield();
}
}
}
public static void main(String[] args) {
//启动了两条并发线程
TestYield ty1=new TestYield(“高级”);
//将ty1线程设置最高优先级
//ty1.setPriority(MAX_PRIORITY);
ty1.start();
TestYield ty2=new TestYield(“低级”);
//ty2.setPriority(MIN_PRIORITY);
ty2.start();
}
}
Sleep方法和yield方法的区别如下:
1. sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同,或优先级更高的线程执行机会。
2. Sleep方法将会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield不会将线程转入阻塞状态,他只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源被执行
3. Sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时,要么捕捉该异常,要么显式声明抛出该异常。而yield方法则没有声明抛出任何异常。
4. Sleep方法比yield更有可移植性,通常不要依靠yield来控制并发线程的执行
改变线程的优先级:
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也有普通优先级。
Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类三个静态常量。
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值是1;
NORM_PRIORITY:其值是5
如:
public class priorityTest extends Thread{
public priorityTest() {}
public priorityTest(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+",其优先级是:"+getPriority()+",循环变量的值为"+i);
}
}
public static void main(String[] args) {
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++) {
if(i10)
{
priorityTest low =new priorityTest(“低级”);
low.start();
System.out.println(“创建之初的优先级:”+low.getPriority());
//设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20)
{
priorityTest high=new priorityTest(“高级”);
high.start();
System.out.println(“创建之初优先级:”+high.getPriority());
//设置该线程为最高优先级
high.setPriority(MAX_PRIORITY);
}
}
}
}
线程同步:
线程安全问题:
如:模拟银行
public class Account {
//封装账号编号,账户余额两个属性
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//下面两个方法根据accountNo来计算Account的hashCode和判断equals
public int hasCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(obj!=null&&obj.getClass()==Account.class)
{
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望取得钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及数据安全问题
public void run() {
//账户余额大于取钱数目
if(account.getBalance()>=drawAmount)
{
System.out.println(getName()+“取钱成功:”+drawAmount);
/try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}/
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println(”\t余额为:"+account.getBalance());
}
else {
System.out.println(getName()+“取钱失败!余额不足”);
}
}
}
public class TestDraw {
public static void main(String[] args) throws Exception {
//创建一个账户
Account acct=new Account(“12356”,1000);
//模拟两个线程对同一个账户取钱
new DrawThread(“爸爸”, acct, 800).start();
Thread.sleep(1000);
new DrawThread(“儿子”, acct, 800).start();
}
}
同步代码块:
当有两个进程并发修改同一个文件时就有可能造成异常。
为了解决这个问题,Java的多线程支持进入了同步监听器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式:
Synchronized(obj)
{
…
//同步代码块
}
上面语法格式中synchronized后括号里的Obj就是同步监听器。上面代码代表的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。
同步监视器的目的:阻止两条线程对同一个共享资源进行并发式访问。因此通常推荐使用可能被访问的共享资源充当同步监视器。
对模拟银行的改动,使用同步监视器:
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望取得钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及数据安全问题
public void run() {
//使用account作为同步监听器,任何线程进入下面同步代码块之前
//必须先获得对account账户的锁定——其他线程无法获得锁,也无法修改它
//这种做法符合:加锁——》修改完成——>释放锁 逻辑
synchronized (account) {
if(account.getBalance()>=drawAmount)
{
System.out.println(getName()+“取钱成功:”+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}
else {
System.out.println(getName()+“取钱失败!余额不足”);
}
}
}
}
通过这种方式就可以保证并发线程在任意时刻只有一条线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一条线程处于临界区内,从而保证了线程的安全性。
同步方法:
对于同步方法而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是对象本身。
通过使用同步方法可以非常方便的将某类变成线程安全的类,线程安全的类具有如下特征:
1. 该类的对象可以被多个线程安全的访问。
2. 每个线程调用该对象的任意方法之后都将得到正确的结果
3. 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
不可变类是安全的,因为它的对象的状态是不可变的。如:
public class Account {
//封装账号编号,账户余额两个属性
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
//下面两个方法根据accountNo来计算Account的hashCode和判断equals
public int hasCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(obj!=null&&obj.getClass()==Account.class)
{
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
public synchronized void draw(double drawAmount)
{
if(balance>=drawAmount)
{
System.out.println(getName()+“取钱成功:”+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//修改余额
balance-=drawAmount;
System.out.println("\t余额为:"+balance);
}
else {
System.out.println(Thread.currentThread().getName()+“取钱失败!余额不足”);
}
}
}
Synchronized关键字可以修改方法,可以修改代码块,但不能修改构造器,属性等。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响。可以采用如下策略:
1. 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源方法进行同步
2. 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为可改变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
释放同步监视器的锁定:
程序无法显式释放对同步代码监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
1. 当线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器
2. 当线程在同步代码块,同步方法中遇到break,return终止了该代码块,该方法的继续执行,当前线程会释放同步监视器。
3. 当线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致了该该代码块,该方法异常结束时将会释放同步监视器。
4. 当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
在下面线程不会释放同步监视器:
1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
2. 线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。我们应该尽量避免suspend和remuse方法来控制线程
同步锁:
Java提供了另外一种同步线程机制:它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。
通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有很大的属性,并且具有差别很大的属性,并且可以支持多个相关的condition对象。
Lock是控制多个线程对共享资源进行访问的工具,通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。在实现线程安全控制中,通常用Reentrant Lock(可重入锁)使用该Lock对象 可以显式的加锁,释放锁,通常使用Lock对象的代码格式如下:
Class X
{
//定义锁对象
Private final ReentrantLock lock=new ReentrantLock();
/…
//定义需要保证线程安全的办法
Public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
//…method body
}
//使用Finally块来保证释放锁
Finally{
Lock.unlock();
}
}
}
使用Lock对象来进行同步时,锁定和释放锁出现在不同作用范围中时,通常建议使用Finally块来确定必要时释放锁。通过使用Lock对象我们可以把Account类改为如下形式,它依然是线程安全的。
public class Account1 {
//定义所对象
private final ReentrantLock lock=new ReentrantLock();
private String accountNo;
private double balance;
public Account1() {}
public Account1(String accountNo,double balance) {
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
//提供了一个线程安全draw方法来完成取钱
public void draw(double drawAmount)
{
//对同步锁进行加锁
lock.lock();
try {
//账户余额大于取钱数目
if(balance>=drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName()+“取钱成功”+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
//修改余额
System.out.println("\t余额为:"+balance);
}
else {
System.out.println(Thread.currentThread().getName()+“余额不足”);
}
}finally {
lock.unlock();
}
}
public int hasCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(obj!=null&&obj.getClass()==Account.class)
{
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
使用Lock时显式使用Lock对象作为同步锁,而使用同步方式时系统隐式使用当前对象作为监视器,使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一条线程进入临界区。
Lock提供了同步方法和同步代码块没有的功能,包括用于非块结构的tryLock方法,以及试图获取可中断锁lockIntreeuptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock锁具有重入性,也就是说线程可以对他已经加锁的ReentrantLock锁再次加锁,Reentrant Lock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
死锁:
当两个线程相互等待对方释放同步监视器时就会发生死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。如:
public class A {
public synchronized void foo(B b)
{
System.out.println(“当前线程名:”+Thread.currentThread().getName()+“进入了A实例foo方法”);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(“当前线程名:”+Thread.currentThread().getName()+“企图调用B实例的last方法”);
b.last();
}
public synchronized void last()
{
System.out.println(“进入了A类的last方法内部”);
}
}
public class B{
public synchronized void bar(B b)
{
System.out.println(“当前线程名:”+Thread.currentThread().getName()+“进入了A实例last方法”);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(“当前线程名:”+Thread.currentThread().getName()+“企图调用A实例的last方法”);
b.last();
}
public synchronized void last()
{
System.out.println(“进入了B类的last方法内部”);
}
}
public class DeadLock implements Runnable{
A a=new A();
B b=new B();
public void intit()
{
Thread.currentThread().setName(“主线程”);
a.foo(b);
System.out.println(“进入主线程之后”);
}
public void run()
{
Thread.currentThread().setName(“主线程”);
a.foo(b);
System.out.println(“进入副线程之后”);
}
public static void main(String[] args) {
DeadLock d1=new DeadLock();
//以d1为target启动新线程
new Thread(d1).start();
d1.intit();
}
}