线程
#程序、进程与线程的区别
- 程序(program)是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。为实现预期目的而进行操作的一系列语句和指令。
- 进程(process)是一个可并发执行的具有独立功能的程序一次执行过程。引进进程是为了提高计算机的运行效率而非速度。各个进程具有自己独立的内存空间。
- 线程(thread)是进程中能够独立执行的实体,是进程的组成部分。引进线程是为了提高进程的执行效率,线程之间共享堆和代码,栈独立。
#多线程机制
许多程序中一些独立的代码段的执行可以彼此重叠,从而获得执行效率,那么可以把这些代码段设计为线程。
多任务处理被所有的现代操作系统所支持。然而,多任务处理有两种截然不同的类型:
- 基于进程的
- 基于进程的多任务处理允许你的计算机同时运行两个或更多的程序。举例来说,基于进程的多任务处理使你在运用文本编辑器的时候可以同时运行Java编译器。
- 在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。
- 基于线程的
- 基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以同时执行两个或者多个任务的功能。例如,一个文本编辑器可以在打印的同时格式化文本。
- 多线程程序包含两条或两条以上并发运行的部分,程序中每个这样的部分都叫做一个线程(Thread)。每个线程都有独立的执行过程,因此多线程是多任务处理的一种特殊形式。
- 每个java程序都有一个主线程,即main()对应的线程。要实现多线程,必须在主线程中创建新的线程。
- 线程是一个进程内部的并发执行,诸线程的所需数据可能是共享的,代码的执行则是分立的
- 区别如图所示:
#线程的生命周期
在Java中,任何对象都有从创建到清除的过程,线程亦是如此。线程从创建到执行完毕并被清除的整个过程称为线程的生命周期。它分为五个阶段,分别为新建状态、就绪状态、运行状态、阻塞状态和死亡状态。线程的这几个状态有个转换过程,如图:
通过new创建线程对象,是的线程处于新建状态,处于该状态的线程尚无法被系统调度执行。执行线程的start方法,使得线程处于就绪状态,处于就绪状态的线程可以被系统调度执行,一旦被调度执行,该线程就处于运行状态。线程在运行状态时,可能因为某种原因,如等待系统资源而被系统阻塞执行,而处于阻塞状态。处于阻塞状态的线程在引起其阻塞的原因消除以后,会被系统转入就绪状态,再次等待被系统调度。一旦线程执行完毕,线程就会处于死亡状态,处于死亡状态的线程是不会重新进入就绪状态,它会被系统在某个时刻清理出内存。
在发生以下情况时,就会阻塞线程的运行:
- 线程体中调用了yield()方法,让出了对CPU的占用权。
- 线程体中调用了sleep()方法,使线程进入睡眠状态。
- 线程由于I/O操作而受阻塞。
- 另一个更高优先级的线程出现。
- 在支持时间片的系统中,该线程的时间片用完。
Java对线程的调度采用的是抢占式调度模型,让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的的线程,随机选择一个线程使其占用CPU,当它失去CPU的使用权后,再随机选择其它线程获得CPU的使用权。大多数情况下程序员无需关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。
#多线程的实现
#创建线程的方式
- 一种是通过创建
Thread
类的子类,该子类应重写Thread
类的run
方法。之后创建这个子类的对象并调用start()
方法。Thread
类实现了Runnable
接口。例如:
class TestThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println("run--"+i);
}
}
public static void main(String[] arg){
TestThread tt=new TestThread();//创建线程对象,此时线程处于新建状态
tt.start();//启动线程,此时线程处于就绪状态,随时会被系统调度进入运行状态。
//main线程只是启动了tt线程,然后tt线程就同main线程没有关系了,它们就是不同的执行实体
for(int i=0;i<10;i++){
System.out.println("main--"+i);
}
}
}
- 另一种是定义实现 Runnable 接口的类。该类实现 run 方法。然后可以创建该类的实例,在创建 Thread对象时作为一个参数来传递并启动。
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();//将p作为Thread的参数,新建一个线程对象
或写作
new Thread(new PrimeRun(143)).start();
#线程的名字
当生成一个线程对象时,如果没有为其指定名字,那么线程对象的名字将使用如下形式:Thread-number,该number是自动增加的数字。 下面两个方法设置与获取线程的名字
public final String getName()返回该线程的名称。
public final void setName(String name)改变线程名称,使之与参数 name 相同。
#获取正在运行的线程
public static Thread currentThread()返回对当前正在执行的线程对象的引用。
#停止线程
线程的消亡不能通过调用stop()命令,而是让run()方法自然结束。stop()方法是不安全的,已经废弃。停止线程推荐的方式:设定一个标志变量,在run()方法中是一个循环,由该标志变量控制循环是继续执行还是跳出;循环跳出,则线程结束。
#线程的种类
线程分为:
- 用户线程User
- 通常我们创建的线程,如果不修改它为守护线程,它就是用户线程,JVM在所有的用户线程终止后方才退出。
- 守护线程Daemon
- JVM无需等待守护线程是否退出,只要所有用户线程退出后就可退出。当JVM中所有的线程都是守护线程的时候,即使守护线程的
run()
方法中还有需要执行的语句,JVM也会退出;如果还有一个或以上的非守护线程则不会退出。 - 守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续运行。值得一提的是,当你在一个守护线程中产生了其他线程,那么这些新产生的线程不用设置
Daemon属性
,都将是守护线程。
- JVM无需等待守护线程是否退出,只要所有用户线程退出后就可退出。当JVM中所有的线程都是守护线程的时候,即使守护线程的
setDaemon(boolean on)
方法可以设置线程的Daemon模式
,true
为Daemon模式
,false
为User模式
。setDaemon(boolean on)
方法必须在线程启动之前调用,当线程正在运行时调用会产生异常。
isDaemon()
方法将测试该线程是否为守护线程。
示例:
/*
守护线程.
其他所有的用户线程结束,则守护线程退出!
守护线程一般都是无限执行的.
*/
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1 = new Processor();
t1.setName("t1");
//将t1这个用户线程修改成守护线程.
t1.setDaemon(true);//必须放在start()之前
t1.start();
//主线程
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
Thread.sleep(1000);
}
}
}
class Processor extends Thread{
public void run(){
int i = 0;
while(true){
i++;
System.out.println(Thread.currentThread().getName()+"-->"+i);
try{Thread.sleep(500);}catch(Exception e){}
}
}
}
#线程的优先级
Java中线程的调度是使用抢先式调度,高优先级优先调度,优先级相同,随机调度。一个线程的优先级设置遵从以下原则:
- 线程创建时,子继承父的优先级。
- 线程创建后,可通过调用setPriority()方法改变优先级。
- 线程的优先级是1-10之间的正整数。
- 1- MIN_PRIORITY
- 10- MAX_PRIORITY
- 5- NORM_PRIORITY
上面三个值在Thread中定义为静态的常量,如果没有设置线程的优先级,默认值是5。我们不能依靠线程的优先级来决定线程的执行顺序,因为由系统来根据优先级来决定执行的顺序是不可靠的。 示例:
/*
线程优先级高的获取的CPU时间片相对多一些。
优先级:1-10
最低 1
最高 10
默认 5
*/
public class ThreadTest{
public static void main(String[] args){
System.out.println(Thread.MAX_PRIORITY); //10
System.out.println(Thread.MIN_PRIORITY); //1
System.out.println(Thread.NORM_PRIORITY); //5
Thread t1 = new Processor();
t1.setName("t1");
Thread t2 = new Processor();
t2.setName("t2");
System.out.println(t1.getPriority()); //5
System.out.println(t2.getPriority()); //5
//设置优先级
t1.setPriority(5);
t2.setPriority(6);
//启动线程
t1.start();
t2.start();
}
}
class Processor extends Thread{
public void run(){
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"--->" + i);
}
}
}
#线程的睡眠
通过调用线程的sleep方法,可以使得当前正在运行的线程睡眠指定的时间,但是睡眠过后的线程仍然需要经过系统的调度方能进入运行状态。
public static void sleep(long millis) throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。在sleep 时间间隔期满后,线程不一定立即恢复执行。t.sleep会阻塞t吗?(t为线程引用)public static void sleep(long millis, int nanos) throws InterruptedException
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)。- 如果任何线程中断了当前线程会抛出: InterruptedException。当抛出该异常时,当前线程的中断状态被清除。
示例:
/*
1.Thread.sleep(毫秒);
2.sleep方法是一个静态方法.
3.该方法的作用:阻塞当前线程.腾出CPU,让给其他线程。
*/
public class ThreadTest{
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Processor();
t1.setName("t1");
t1.start();
//阻塞主线程
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
Thread.sleep(500);
}
}
}
class Processor extends Thread{
//Thread中的run方法不抛出异常,所以重写run方法之后,在run方法的声明位置上不能使用throws
//所以run方法中的异常只能try...catch...
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
try{
Thread.sleep(1000); //让当前线程阻塞1S。
}catch(InterruptedException e){
e.printStackTrace();
}
}
//m1();
}
/*
//m1方法是可以使用throws的.
public void m1() throws Exception{
}
*/
}
#中断线程
在Java 线程机制中,用stop结束线程是不安全的,我们可以采用另一种结束线程的方式,那就是中断。Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。简而言之就是让线程外部可以设置一个标记值,而线程内部在执行时可以检查这个值,来获知此线程是否应该结束了。
常用的处理中断的方法有下面三个:
public void interrupt()
中断线程。public static boolean interrupted()
测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。public boolean isInterrupted()
测试线程是否已经中断。线程的中断状态不受该方法的影响
示例:
/*
某线程正在休眠,如果打断它的休眠.
以下方式依靠的是异常处理机制。
*/
public class ThreadTest08
{
public static void main(String[] args) throws Exception{
//需求:启动线程,5S之后打断线程的休眠.
Thread t = new Thread(new Processor());
//起名
t.setName("t");
//启动
t.start();
//5S之后
Thread.sleep(5000);
//打断t的休眠.
t.interrupt();
}
}
class Processor implements Runnable
{
public void run(){
try{
Thread.sleep(100000000000L);
System.out.println("HelloWorld!");
}catch(InterruptedException e){
//e.printStackTrace();
}
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
/*
如何正确的更好的终止一个正在执行的线程.
需求:线程启动5S之后终止。
*/
public class ThreadTest09
{
public static void main(String[] args) throws Exception{
Processor p = new Processor();
Thread t = new Thread(p);
t.setName("t");
t.start();
//5S之后终止.
Thread.sleep(5000);
//终止
p.run = false;
}
}
class Processor implements Runnable
{
boolean run = true;
public void run(){
for(int i=0;i<10;i++){
if(run){
try{Thread.sleep(1000);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"-->" + i);
}else{
return;
}
}
}
}
#线程插队
通过调用某个线程的join方法,比如,t.join()
,可以使得该语句所在的线程等待t线程执行结束,方可继续执行,相当于t线程的执行合并到该线程的执行流程中去。
public final void join() throws InterruptedException
等待该线程终止。public final void join(long millis) throws InterruptedException
等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。public final void join(long millis, int nanos) throws InterruptedException
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。- 如果任何线程中断了当前线程则抛出:InterruptedException。当抛出该异常时,当前线程的中断状态被清除。
示例:
/*
线程的合并
*/
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t = new Thread(new Processor());
t.setName("t");
t.start();
//合并线程
t.join(); //t和主线程合并. 单线程的程序.
//主线程
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Processor implements Runnable{
public void run(){
for(int i=0;i<5;i++){
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
示例:
public class TestJoin{
public static void main(String args[]){
TJ t=new TJ();
t.t1.start();
}
}
class TJ implements Runnable{
Thread t1,t2;
TJ(){
t1=new Thread(this);
t2=new Thread(this);
}
public void run(){
if(Thread.currentThread()==t1){
System.out.println("t1:我在等着你的到来!");
t2.start();
//while(!t2.isAlive()){}
try{
t2.join();
System.out.println("t1:不要让我等到花儿也谢了!");
}catch(InterruptedException e){}
}else System.out.println("t2:你已经来了吗?");
}
}
#线程的让步
yield()应该做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()不会导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到就绪状态。 public static void yield()
暂停当前正在执行的线程对象,并执行其他(包含刚被暂停的线程)线程。
示例:
/*
Thread.yield();
1.该方法是一个静态方法.
2.作用:给同一个优先级的线程让位。但是让位时间不固定。
3.和sleep方法相同,就是yield时间不固定。
*/
public class ThreadTest{
public static void main(String[] args){
Thread t = new Processor();
t.setName("t");
t.start();
//主线程中
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Processor extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
if(i%20==0){
Thread.yield();
}
}
}
}
#线程的同步
线程的编程模式有:
- 异步编程模型:t1线程执行t1的,t2线程执行t2的,两个线程之间谁也不等谁。
- 同步编程模型:t1线程和t2线程的执行相互之间有影响,比如,当t1线程必须等t2线程执行结束之后,t1线程才能执行,这是同步编程模型。
什么条件下要使用线程同步?
- 第一:必须是多线程环境
- 第二:并发执行的多个线程间需要共享资源或交换数据,则这一组线程称为交互线程。交互线程之间存在两种关系:
竞争关系
和协作关系
。线程间需要解决共享资源冲突问题因而采用线程同步。 - 第三:共享的数据涉及到修改操作。
示例:
public class ThreadTest{
public static void main(String[] args){
//创建一个公共的账户
Account act = new Account("actno-001",5000.0);
//创建线程对同一个账户取款
Thread t1 = new Thread(new Processor(act));
Thread t2 = new Thread(new Processor(act));
t1.start();
t2.start();
}
}
//取款线程
class Processor implements Runnable{
//账户
Account act;
//Constructor
Processor(Account act){
this.act = act;
}
public void run(){
act.withdraw(1000.0);
System.out.println("取款1000.0成功,余额:" + act.getBalance());
}
}
//账户
class Account{
private String actno;
private double balance;
public Account(){}
public Account(String actno,double balance){
this.actno = actno;
this.balance = balance;
}
//setter and getter
public void setActno(String actno){
this.actno = actno;
}
public void setBalance(double balance){
this.balance = balance;
}
public String getActno(){
return actno;
}
public double getBalance(){
return balance;
}
//对外提供一个取款的方法
public void withdraw(double money){ //对当前账户进行取款操作
double after = balance - money;
//延迟
try{Thread.sleep(1000);}catch(Exception e){}
//更新
this.setBalance(after);
}
}
并发线程之间可能是无关的,也可能是交互的。并发执行的多个线程间需要共享资源或交换数据,则这一组线程称为交互线程
。交互线程之间存在两种关系:竞争关系
和协作关系
。
#线程的竞争
线程的资源竞争出现了俩个问题:一是死锁;二是饥饿。线程间需要解决共享资源冲突问题因而采用线程同步
。
线程互斥
是指若干个线程使用同一共享资源时,任意时刻允许最多一个线程去使用,该共享资源称为临界资源
,操作共享资源的代码称为临界区
。
系统对进入临界区的线程调度原则:
- 一次至多一个线程在它的临界区
- 不能让一个线程无限的留在它的临界区
- 不能强迫一个线程无限的等待进入它的临界区
- 总之,无空等待、有空让进、择一而入、算法可行
对synchronized
的使用包括两种用法:synchronized
方法和 synchronized
块。
#synchronized 方法
Java中的每个对象都有一个锁(lock),当一个线程访问某个对象的synchronized
关键字修饰的方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized
方法了。直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized
方法。
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void method(){
//…
}
利用synchronized
方法控制对其所在类的成员变量的访问:每个对象对应一把锁,每个 synchronized
方法都必须获得调用该方法的对象的锁方能获得执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。
这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized
的成员方法中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized
)。
示例:
/*
以下程序使用线程同步机制保证数据的安全。
*/
public class ThreadTest{
public static void main(String[] args){
//创建一个公共的账户
Account act = new Account("actno-001",5000.0);
//创建线程对同一个账户取款
Thread t1 = new Thread(new Processor(act));
Thread t2 = new Thread(new Processor(act));
t1.start();
t2.start();
}
}
//取款线程
class Processor implements Runnable{
//账户
Account act;
//Constructor
Processor(Account act){
this.act = act;
}
public void run(){
act.withdraw(1000.0);
System.out.println("取款1000.0成功,余额:" + act.getBalance());
}
}
//账户
class Account{
private String actno;
private double balance;
public Account(){}
public Account(String actno,double balance){
this.actno = actno;
this.balance = balance;
}
//setter and getter
public void setActno(String actno){
this.actno = actno;
}
public void setBalance(double balance){
this.balance = balance;
}
public String getActno(){
return actno;
}
public double getBalance(){
return balance;
}
//对外提供一个取款的方法
//synchronized关键字添加到成员方法上,线程拿走的也是this的对象锁。
public synchronized void withdraw(double money){ //对当前账户进行取款操作
//把需要同步的代码,放到同步语句块中.
/*
原理:t1线程和t2线程.
t1线程执行到此处,遇到了synchronized关键字,就会去找this的对象锁,
如果找到this对象锁,则进入同步语句块中执行程序。当同步语句块中的代码
执行结束之后,t1线程归还this的对象锁。
在t1线程执行同步语句块的过程中,如果t2线程也过来执行以下代码,也遇到
synchronized关键字,所以也去找this的对象锁,但是该对象锁被t1线程持有,
只能在这等待this对象的归还。
*/
double after = balance - money;
//延迟
try{Thread.sleep(1000);}catch(Exception e){}
//更新
this.setBalance(after);
}
}
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员方法声明为 synchronized
,以控制其对类的静态成员变量的访问。
/*
类锁,类只有一个,所以锁是类级别的,只有一个.
*/
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new Processor());
Thread t2 = new Thread(new Processor());
t1.setName("t1");
t2.setName("t2");
t1.start();
//延迟,保证t1先执行
Thread.sleep(1000);
t2.start();
}
}
class Processor implements Runnable{
public void run(){
if("t1".equals(Thread.currentThread().getName())){
MyClass.m1();
}
if("t2".equals(Thread.currentThread().getName())){
MyClass.m2();
}
}
}
class MyClass{
//synchronized添加到静态方法上,线程执行此方法的时候会找类锁。
public synchronized static void m1(){
try{Thread.sleep(10000);}catch(Exception e){}
System.out.println("m1....");
}
//不会等m1结束,因为该方法没有被synchronized修饰
/*
public static void m2(){
System.out.println("m2...");
}
*/
//m2方法等m1结束之后才能执行,该方法有synchronized
//线程执行该代码需要“类锁”,而类锁只有一个。
public synchronized static void m2(){
System.out.println("m2...");
}
}
#synchronized 块
通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码
}
synchronized 块
是这样一个代码块,其中的代码必须获得引用类型对象 syncObject
(可以是对象或类)的锁方能执行。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。如果需对类上锁,则形如:
synchronized(Foo.class) {
临界代码
}
示例:
/*
以下程序使用线程同步机制保证数据的安全。
*/
public class ThreadTest{
public static void main(String[] args){
//创建一个公共的账户
Account act = new Account("actno-001",5000.0);
//创建线程对同一个账户取款
Thread t1 = new Thread(new Processor(act));
Thread t2 = new Thread(new Processor(act));
t1.start();
t2.start();
}
}
//取款线程
class Processor implements Runnable{
//账户
Account act;
//Constructor
Processor(Account act){
this.act = act;
}
public void run(){
act.withdraw(1000.0);
System.out.println("取款1000.0成功,余额:" + act.getBalance());
}
}
//账户
class Account{
private String actno;
private double balance;
public Account(){}
public Account(String actno,double balance){
this.actno = actno;
this.balance = balance;
}
//setter and getter
public void setActno(String actno){
this.actno = actno;
}
public void setBalance(double balance){
this.balance = balance;
}
public String getActno(){
return actno;
}
public double getBalance(){
return balance;
}
//对外提供一个取款的方法
public void withdraw(double money){ //对当前账户进行取款操作
//把需要同步的代码,放到同步语句块中.
/*
原理:t1线程和t2线程.
t1线程执行到此处,遇到了synchronized关键字,就会去找this的对象锁,
如果找到this对象锁,则进入同步语句块中执行程序。当同步语句块中的代码
执行结束之后,t1线程归还this的对象锁。
在t1线程执行同步语句块的过程中,如果t2线程也过来执行以下代码,也遇到
synchronized关键字,所以也去找this的对象锁,但是该对象锁被t1线程持有,
只能在这等待this对象的归还。
*/
synchronized(this){
double after = balance - money;
//延迟
try{Thread.sleep(1000);}catch(Exception e){}
//更新
this.setBalance(after);
}
}
}
上例中,锁syncObject这个对象,谁拿到这个锁谁就能够运行他所控制的那段代码。当有一个明确的对象作为锁时,可以这样写,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特别的instance变量(它得是个对象)来充当锁:
class Foo implements Runnable{
private byte[] lock = new byte[0]; // 特别的instance变量
public void method() {
synchronized(lock) { //… }
}
//…..
}
注:创建零长度的byte数组对象将比创建任何对象都经济,查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
将synchronized作用于static 方法,示例代码如下:
class Foo {
public synchronized static void method1(){ // 同步的static 方法
//….
}
public void method2() {
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的method2()方法是把class作为锁的情况,他和synchronized修饰的static方法产生的效果是相同的,取得的锁很特别,是当前被调用方法的所属的类(而不再是由这个类产生的某个具体对象了)。 类名.class
是Class对象的引用,每个被加载的类,在jvm中都会有一个Class对象与之相对应。
#线程的协作
线程可使用Object
中的wait()
,notify()
,notifyAll()
方法实现线程间协作同步。
线程交互时,如果线程对一个同步对象x
发出一个wait()
调用,该线程会暂停执行,进入等待状态,直到其他线程调用该同步对象x的notify
或notifyAll
,该线程被唤醒。
要确保调用wait()
方法的时候拥有锁,即,wait()
方法的调用必须放在synchronized
方法或synchronized
块中。
线程调用wait()
方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()
或者notifyAll()
方法),这样它才能重新获得锁的拥有权和恢复执行。
notify()
和wait()
, 是属于最底层的object
基础类的, 所以每个对象都有notify
和wait
的功能。因为他们是用来操纵锁的,而每个对象都有锁,锁是每个对象的基础。
示例:
class TestWait{
public static void main(String[] args){
ThreadB b=new ThreadB();
synchronized(b){//括号里的b是什么意思,起什么作用?
try{
b.start();
System.out.println("b is start....");
System.out.println("Waiting for b to complete...");
b.wait();//这一句是什么意思,究竟谁在wait?
//System.out.println(getState());
System.out.println("Completed.Now back to main thread");
}
catch (InterruptedException e){}
catch(Exception e){}
}
System.out.println("Total is :"+b.total);
}
}
class ThreadB extends Thread{
int total;
public void run() {
synchronized(this){
System.out.println("ThreadB is running..");
for (int i=0;i<100;i++ ) {
total +=i;
}
System.out.println("total is "+total);
notify();
}
}
}
使用同步,就要锁定对象,也就是获取对象锁,其它要使用该对象锁的线程都只能排队等着, 等到同步方法或者同步块里的程序全部运行完才有机会。在同步方法和同步块中, sleep()
等不可能自己被调用的时候解除锁定,他们都霸占着正在使用的对象锁不放。
wait
可以让同步方法或者同步块暂时放弃对象锁,而将它暂时让给其它需要对象锁的线程用,这意味着可在执行wait()
期间调用线程对象中的其他同步方法!在其它情况下(如:sleep) 这是不可能的。
示例:
class TW{
public static void main(String[] args) throws InterruptedException{
Object lock = new Object();
ThreadA t = new ThreadA();
t.setLock(lock);
synchronized(lock){
t.start();
lock.wait();
System.out.println("in main");
}
}
}
class ThreadA extends Thread{
Object lock;
public void setLock(Object lock){
this.lock = lock;
}
public void run(){
synchronized(lock){
System.out.println("in thread");
try{
Thread.sleep(1000);
//notify();
//lock.notify();
}catch(Exception e){}
}
}
}
//上面的例子main会一直等待唤醒,而下面的例子中当子线程终止以后,main会被唤醒,
//这是因为一个线程执行结束以后,
//会执行notifyAll方法,因此不建议在线程上使用wait及notify与notifyAll
class TW{
public static void main(String[] args) throws InterruptedException{
//Object lock = new Object();
ThreadA t = new ThreadA();
t.setLock(t);//把线程t设置为了锁
synchronized(t){
t.start();
t.wait();
System.out.println("in main");
}
}
}
class ThreadA extends Thread{
Object lock;
public void setLock(Object lock){
this.lock = lock;
}
public void run(){
synchronized(lock){
System.out.println("in thread");
try{
Thread.sleep(1000);
}catch(Exception e){}
}
}
}
notify()
方法
notify()
方法会唤醒一个等待当前对象的锁的线程。- 如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了
wait
方法中的一个)。 - 被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。
- 被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。也就是说,被唤醒的线程并没有什么优先权,也没有什么劣势,对象的下一个线程还是需要通过一般性的竞争。
notify()
方法应该是被拥有对象的锁的线程所调用。换句话说,和wait()
方法一样,notify
方法调用必须放在synchronized
方法或synchronized
块中。
一个线程变为一个对象的锁的拥有者是通过下列三种方法:
- 执行这个对象的synchronized实例方法。
- 执行这个对象的synchronized语句块。这个语句块锁的是这个对象。
- 对于Class类的对象,执行那个类的synchronized的static方法。
示例:
银行账户问题 从同一个账户取款和存款,如果发生时间的交叉,就可能出现数据不正确的情况
import java.util.Random;
class Account{
private double balance = 1000;
public double getBalance(){
return balance;
}
public void setBalance(double balance){
this.balance = balance;
}
public void withdraw(String role, double amount) throws InterruptedException{
double balance = getBalance();
System.out.println(role + ":Now the balance is " + balance + ", I want to withdraw " + amount);
//Thread.sleep(new Random().nextInt(500) + 500);
Thread.sleep(500);
setBalance(balance - amount);
System.out.println(role + ":After " + balance + " Now the balance is " + getBalance());
}
public void deposit(String role, double amount) throws InterruptedException{
double balance = getBalance();
System.out.println(role + ":Now the balance is " + balance + ", I want to deposit " + amount);
//Thread.sleep(new Random().nextInt(500) + 500);
Thread.sleep(500);
setBalance(balance + amount);
System.out.println(role + ":After " + balance + " Now the balance is " + getBalance());
}
}
class Person extends Thread{
Account ac;
String role;
double totalWithdraw = 0;
double totalDeposit = 0;
static boolean flag = true;
Person(String role, Account ac){
this.role = role;
this.ac = ac;
}
public void run(){
try{
while(flag){
if(role.equals("child")){
totalWithdraw += 15;
ac.withdraw(role, 15);
} else if(role.equals("father")){
totalDeposit += 15;
ac.deposit(role, 15);
}
}
if(role.equals("child")){
System.out.println("Total withdraw amount is " + totalWithdraw);
} else if(role.equals("father")){
System.out.println("Total deposit amount is " + totalDeposit);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestBank{
public static void main(String[] args) throws InterruptedException{
Account ac = new Account();
Person p1 = new Person("father", ac);
Person p2 = new Person("child", ac);
p1.start();
p2.start();
Thread.sleep(2000);
Person.flag = false;
Thread.sleep(1000);
System.out.println("Now the balance is " + ac.getBalance());
}
}
生产者消费者问题 生产者将产品投入仓库,消费者从仓库中取产品。当仓库中的产品不够消费者使用时,消费者等待并通知生产者生产产品并放入仓库,生产者放入产品以后通知消费者取产品。如此,循环往复。
import java.util.Random;
class Store{
int product = 100;
final static int capacity = 100;
//private byte[] lock = new byte[0];
public void produce(){
synchronized(this){
while(product >= capacity){
try{
wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("当前库存量为:" + product);
System.out.println("我正在生产中。。。。");
product = capacity;
System.out.println("生产完成,已放入仓库中");
System.out.println();
notifyAll();
}
}
public void consume(){
//int amount;
synchronized(this){
//while((amount = Random.nextInt(50)) > product)
int amount = new Random().nextInt(50);
System.out.println("当前库存量为:" + product);
System.out.println("我需要:" + amount);
while (product < amount){
try{
System.out.println("不够,我得等待了");
wait();
System.out.println("够吗?");
}catch(InterruptedException e){
e.printStackTrace();
}
}
product = product - amount;
System.out.println("我已消费了:" + amount);
System.out.println("还剩:" + product);
System.out.println();
notifyAll();
}
}
}
class Producer extends Thread{
Store store;
public boolean flag = true;
Producer(Store store){
this.store = store;
}
public void run(){
while(flag){
store.produce();
}
}
}
class Consumer extends Thread{
Store store;
public boolean flag = true;
Consumer(Store store){
this.store = store;
}
public void run(){
while(flag){
store.consume();
}
}
}
public class TestPC{
public static void main(String[] args) throws InterruptedException{
Store store = new Store();
Producer p = new Producer(store);
Consumer c1 = new Consumer(store);
Consumer c2 = new Consumer(store);
p.start();
c1.start();
c2.start();
Thread.sleep(1);
p.flag = false;
c1.flag = false;
c2.flag = false;
}
}
#一些常见问题
- 线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字不确定。
- 线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
- 获取当前线程的对象的方法是:Thread.currentThread();
- 一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
- 当线程中
run()
方法结束时该线程完成。 - 一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程可以被重新调度。
- 线程的调度是JVM的一部分,在一个机器上,实际上一次只能运行一个线程。一次只有一个线程执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。
- 众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。
- 尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列。
- 尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。