Java多线程基础总结详解编程语言

一、进程与线程

1、进程

     进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动。操作系统中,几乎所有运行中的任务对应一条进程(Process)。一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。描述进程的有一句话非常经典的话——进程是系统进行资源分配和调度的一个独立单位。

     进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间进程的实质,就是程序在多道程序系统中的一次执行过程,它是动态产生,动态消亡的,具有自己的生命周期和各种不同的状态。进程具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进。 

(注意,并发性(concurrency)和并行性(parallel)是不同的。并行指的是同一时刻,多个指令在多台处理器上同时运行。并发指的是同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,看起来就好像多个指令同时执行一样。

    进程由程序数据进程控制块三部分组成。

2、线程

     线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

   
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

    在Java Web中要注意,线程是JVM级别的,在不停止的情况下,跟JVM共同消亡,就是说如果一个Web服务启动了多个Web应用,某个Web应用启动了某个线程,如果关闭这个Web应用,线程并不会关闭,因为JVM还在运行,所以别忘了设置Web应用关闭时停止线程。

二、线程的生命周期及五种基本状态

关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

Java多线程基础总结详解编程语言

上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。

Java线程具有五种基本状态

新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread()。

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞 — 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、创建多线程的方式

Java中线程的创建有如下三种基本形式。

1、继承Thread类,重写该类的run()方法。

package com.demo.test; 
 
public class MyThread extends Thread { 
     
    private String name; 
     
    public MyThread(String name){ 
        this.name = name; 
    } 
 
    @Override 
    public void run() { 
        for (int i = 0; i < 10; i++) { 
            System.out.println(name + "运行  :" + i); 
        } 
    } 
}
package com.demo.test; 
 
public class ThreadTest { 
 
     public static void main(String[] args) { 
         Thread myThread1 = new MyThread("A");     // 创建一个新的线程  myThread1  此线程进入新建状态 
         Thread myThread2 = new MyThread("B");     // 创建一个新的线程 myThread2 此线程进入新建状态 
         myThread1.start();  // 调用start()方法使得线程进入就绪状态 
         myThread2.start();  // 调用start()方法使得线程进入就绪状态 
    } 
}

运行结果:

A运行  :0 
B运行  :0 
B运行  :1 
B运行  :2 
B运行  :3 
B运行  :4 
B运行  :5 
B运行  :6 
B运行  :7 
B运行  :8 
A运行  :1 
A运行  :2 
A运行  :3 
B运行  :9 
A运行  :4 
A运行  :5 
A运行  :6 
A运行  :7 
A运行  :8 
A运行  :9

     如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

2、实现java.lang.Runnable接口 

具体做法:实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

package com.demo.test; 
 
public class MyRunnable implements Runnable{ 
 
    private String name; 
     
    public MyRunnable(String name){ 
        this.name = name; 
    } 
 
    @Override 
    public void run() { 
        for (int i = 0; i < 10; i++) { 
            System.out.println(name + "运行  :" + i); 
        } 
    } 
}
package com.demo.test; 
 
public class ThreadTest { 
 
     public static void main(String[] args) { 
 
         Runnable myRunnable = new MyRunnable("A"); // 创建一个Runnable实现类的对象 
         Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程 
         Runnable myRunnable1 = new MyRunnable("B"); 
         Thread thread2 = new Thread(myRunnable1); 
         thread1.start(); // 调用start()方法使得线程进入就绪状态 
         thread2.start(); 
    } 
}

运行结果:

A运行  :0 
B运行  :0 
B运行  :1 
B运行  :2 
A运行  :1 
A运行  :2 
B运行  :3 
A运行  :3 
A运行  :4 
B运行  :4 
A运行  :5 
A运行  :6 
A运行  :7 
B运行  :5 
A运行  :8 
A运行  :9 
B运行  :6 
B运行  :7 
B运行  :8 
B运行  :9

3、使用Callable和Future接口创建线程。

具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

package com.demo.test; 
 
import java.util.concurrent.Callable; 
 
public class MyCallable implements Callable<Integer>{ 
     
    // 与run()方法不同的是,call()方法具有返回值 
    @Override 
    public Integer call() throws Exception{ 
        System.out.println("子线程在进行计算"); 
        Thread.sleep(3000); 
        int sum = 0; 
        for (int i = 0; i < 100; i++) { 
            //System.out.println(Thread.currentThread().getName() + " " + i); 
            sum += i; 
        } 
        return sum; 
    } 
 
}
package com.demo.test; 
 
import java.util.concurrent.Callable; 
import java.util.concurrent.ExecutionException; 
import java.util.concurrent.FutureTask; 
 
public class ThreadTest { 
 
     public static void main(String[] args) { 
          
         Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象 
         FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象 
 
         Thread thread = new Thread(ft);  //FutureTask对象作为Thread对象的target创建新的线程 
         thread.start();  //线程进入到就绪状态 
 
         try { 
             Thread.sleep(1000); 
         } catch (InterruptedException e1) { 
             e1.printStackTrace(); 
         } 
          
         System.out.println("主线程在执行任务"); 
         
         try { 
             int sum = ft.get(); //取得新创建的线程中的call()方法返回的结果 
             System.out.println("task运行结果,sum = " + sum); 
         } catch (InterruptedException e) { 
             e.printStackTrace(); 
         } catch (ExecutionException e) { 
             e.printStackTrace(); 
         } 
          
         System.out.println("所有任务执行完毕"); 
         
    } 
}

运行结果:

子线程在进行计算 
主线程在执行任务 
task运行结果,sum = 4950 
所有任务执行完毕

     首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> { 
    void run(); 
}

     于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

     执行下此程序,我们发现sum = 4950永远都是最后输出的。那么为什么sum =4950会永远最后输出呢?原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

    上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。

四、线程调度

1、线程加入——join()

join —— 让一个线程等待另一个线程完成才继续执行。如A线程执行体中调用B线程的join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行。

join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。

join是Thread类的一个方法,启动线程后直接调用,join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。 

为什么要用join() 方法?

     在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

不加join的情况:

package com.demo.test; 
 
public class Thread1 extends Thread{ 
     
    private String name; 
    public Thread1(String name) { 
        super(name); 
        this.name=name; 
    } 
     
    public void run() { 
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!"); 
        for (int i = 0; i < 5; i++) { 
            System.out.println("子线程"+name + "运行 : " + i); 
            try { 
                sleep((int) Math.random() * 10); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } 
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!"); 
    } 
}
package com.demo.test; 
 
public class Main { 
     
    public static void main(String[] args) { 
 
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); 
        Thread1 mTh1=new Thread1("A"); 
        Thread1 mTh2=new Thread1("B"); 
        mTh1.start(); 
        mTh2.start(); 
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); 
    } 
}

运行结果:

main主线程运行开始! 
A 线程运行开始! 
B 线程运行开始! 
main主线程运行结束! 
子线程B运行 : 0 
子线程A运行 : 0 
子线程B运行 : 1 
子线程A运行 : 1 
子线程B运行 : 2 
子线程A运行 : 2 
子线程B运行 : 3 
子线程A运行 : 3 
子线程B运行 : 4 
子线程A运行 : 4 
B 线程运行结束! 
A 线程运行结束!

加join 的情况:

package com.demo.test; 
 
public class Main { 
     
    public static void main(String[] args) { 
 
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); 
        Thread1 mTh1=new Thread1("A"); 
        Thread1 mTh2=new Thread1("B"); 
        mTh1.start(); 
        mTh2.start(); 
        try { 
            mTh1.join(); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        try { 
            mTh2.join(); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
 
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); 
    } 
}

运行结果:

main主线程运行开始! 
A 线程运行开始! 
B 线程运行开始! 
子线程A运行 : 0 
子线程B运行 : 0 
子线程A运行 : 1 
子线程B运行 : 1 
子线程A运行 : 2 
子线程B运行 : 2 
子线程A运行 : 3 
子线程B运行 : 3 
子线程A运行 : 4 
子线程B运行 : 4 
A 线程运行结束! 
B 线程运行结束! 
main主线程运行结束!

2、线程睡眠——sleep()

sleep()是Thread类的静态方法。该方法声明抛出了InterrupedException异常。所以使用时,要么捕捉,要么声明抛出。有两种重载方式:

static void sleep(long millis); //让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准度的影响。 
 
static void sleep(long millis , int nanos);  //让当前正在执行的线程暂停millis毫秒加nanos微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的
精度和准度的影响。

sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。常用来暂停程序的运行。同时注意,sleep()方法不会释放锁

3、线程让步——yield()

     yield()是Thread类的静态方法。它能让当前线程暂停,但不会阻塞该线程,而是由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权,也有可能是当前线程又进入到“运行状态”继续运行!值得注意的是,yield()方法不会释放锁

package com.demo.test; 
 
public class ThreadYield extends Thread{ 
     
    public ThreadYield(String name) { 
        super(name); 
    } 
  
    @Override 
    public void run() { 
        for (int i = 1; i <= 50; i++) { 
            System.out.println("" + this.getName() + "-----" + i); 
            // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行) 
            if (i ==30) { 
                this.yield(); 
            } 
        } 
    } 
 
}
package com.demo.test; 
 
public class Main { 
     
    public static void main(String[] args) { 
 
        ThreadYield yt1 = new ThreadYield("A"); 
        ThreadYield yt2 = new ThreadYield("B"); 
        yt1.start(); 
        yt2.start(); 
 
    } 
}

运行情况:

第一种情况:A线程当执行到30时会将CPU时间让掉,这时B线程抢到CPU时间并执行。

第二种情况:A线程当执行到30时会将CPU时间让掉,这时A线程抢到CPU时间并执行。

第三种情况:B线程当执行到30时会将CPU时间让掉,这时A线程抢到CPU时间并执行。

第四种情况:B线程当执行到30时会将CPU时间让掉,这时B线程抢到CPU时间并执行。

4、线程中断——interrupt()

     我们经常通过判断线程的中断标记来控制线程。   

  interrupt()是Thread类的一个实例方法,用于中断本线程。这个方法被调用时,会立即将线程的中断标志设置为“true”。所以当中断处于“阻塞状态”的线程时,由于处于阻塞状态,中断标记会被设置为“false”,抛出一个 InterruptedException。所以我们在线程的循环外捕获这个异常,就可以退出线程了。

  interrupt()并不会中断处于“运行状态”的线程,它会把线程的“中断标记”设置为true,所以我们可以不断通过isInterrupted()来检测中断标记,从而在调用了interrupt()后终止线程,这也是通常我们对interrupt()的用法。

  interrupted()是Thread类的一个静态方法,它返回一个布尔类型指明当前线程是否已经被中断,isInterrupted()是Thread类的实例方法,返回一个布尔类型来判断线程是否已经被中断。它们都能够用于检测对象的“中断标记”。区别是,interrupted()除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false);而isInterrupted()仅仅返回中断标记。

综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:

@Override 
public void run() { 
  try { 
      // 1. isInterrupted()保证,只要中断标记为true就终止线程。 
      while (!isInterrupted()) { 
          // 执行任务... 
      } 
  } catch (InterruptedException ie) { 
      // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。 
  } 
}

interrupted() 和 isInterrupted() 的比较

首先看一下API中该方法的实现: 

public static boolean interrupted() {  
    return currentThread().isInterrupted(true);  
} 

该方法就是直接调用当前线程的isInterrupted(true)的方法。

然后再来看一下API中 isInterrupted的实现:

public boolean isInterrupted() {  
    return isInterrupted(false);  
} 

该方法却直接调用当前线程的isInterrupted(false)的方法。

因此这两个方法有两个主要区别:

(1)interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)

(2)这两个方法最终都会调用同一个方法—–isInterrupted( Boolean 参数),只不过参数固定为一个是true,一个是false。注意:isInterrupted( Boolean 参数)是isInterrupted()的重载方法。

由于第二个区别主要体现在调用的方法的参数上,让我们来看一看这个参数是什么含义。先来看一看被调用的方法 isInterrupted(boolean arg)(Thread类中重载的方法)的定义:

private native boolean isInterrupted( boolean ClearInterrupted);
     原来这是一个本地方法,看不到源码。不过没关系,通过参数名ClearInterrupted我们就能知道,这个参数代表是否要清除状态位。如果这个参数为true,说明返回线程的状态位后,要清掉原来的状态位(恢复成原来情况)。这个参数为false,就是直接返回线程的状态位。这两个方法很好区分,只有当前线程才能清除自己的中断位(对应interrupted()方法)。 

五、线程安全与线程同步

1、一个典型的Java线程安全例子

package com.demo.test; 
 
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; 
    } 
 
}
package com.demo.test; 
 
public class DrawMoneyRunnable implements Runnable{ 
     
    private Account account; 
    private double drawAmount; 
 
    public DrawMoneyRunnable(Account account, double drawAmount) { 
        super(); 
        this.account = account; 
        this.drawAmount = drawAmount; 
    } 
 
    public void run() { 
        if (account.getBalance() >= drawAmount) {  //1 
            System.out.println("取钱成功, 取出钱数为:" + drawAmount); 
            double balance = account.getBalance() - drawAmount; 
            account.setBalance(balance); 
            System.out.println("余额为:" + balance); 
        } 
    } 
 
}
package com.demo.test; 
 
public class ThreadTest { 
 
    public static void main(String[] args) { 
        Account account = new Account("123456", 1000); 
        DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); 
        Thread myThread1 = new Thread(drawMoneyRunnable); 
        Thread myThread2 = new Thread(drawMoneyRunnable); 
        myThread1.start(); 
        myThread2.start(); 
    } 
 
}

    上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟两个人同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:

取钱成功, 取出钱数为:700.0 
余额为:300.0 
取钱成功, 取出钱数为:700.0 
余额为:-400.0

     也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。

     经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。

     因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。

2、多线程的同步

为何要使用同步? 

     java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

(1)同步方法

     对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推…。

     在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synchronized关键字即可。

public synchronized void run() { 
        
    // .... 
  
}

(2)同步代码块

     正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

synchronized (obj) { 
             
    //... 
 
}

      其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

      如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心…)

关于synchronized关键字的说明:

 ① 原理

    在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当前线程调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj),当前线程就获取了“obj这个对象”的同步锁。

    不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

② 基本规则

第一条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

第二条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块

第三条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

③ 实例锁和全局锁

实例锁 — 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。

全局锁 — 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

就是说,一个非静态方法上的synchronized关键字,代表该方法依赖其所属对象。一个静态方法上synchronized关键字,代表该方法依赖这个类本身。

六、线程通信 wait()/notify()/notifyAll()

wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

例子:

package com.demo.test; 
 
public class Account { 
 
    private String accountNo; 
    private double balance; 
    // 标识账户中是否已有存款 
    private boolean flag = false; 
 
    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; 
    } 
     
    /** 
     * 存钱 
     *  
     * @param depositeAmount 
     */ 
    public synchronized void deposite(double depositeAmount, int i) { 
 
        if (flag) { 
            // 账户中已有人存钱进去,此时当前线程需要等待阻塞 
            try { 
                System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i); 
                wait(); 
                // 1 
                System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } else { 
            // 开始存钱 
            System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i); 
            setBalance(balance + depositeAmount); 
            flag = true; 
 
            // 唤醒其他线程 
            notifyAll(); 
 
            // 2 
            try { 
                Thread.sleep(3000); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i); 
        } 
    } 
 
    /** 
     * 取钱 
     *  
     * @param drawAmount 
     */ 
    public synchronized void draw(double drawAmount, int i) { 
        if (!flag) { 
            // 账户中还没人存钱进去,此时当前线程需要等待阻塞 
            try { 
                System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i); 
                wait(); 
                System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } else { 
            // 开始取钱 
            System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i); 
            setBalance(getBalance() - drawAmount); 
 
            flag = false; 
 
            // 唤醒其他线程 
            notifyAll(); 
 
            System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); // 3 
        } 
    } 
 
}
package com.demo.test; 
 
public class DrawMoneyThread extends Thread{ 
     
    private Account account; 
    private double amount; 
 
    public DrawMoneyThread(String threadName, Account account, double amount) { 
        super(threadName); 
        this.account = account; 
        this.amount = amount; 
    } 
 
    public void run() { 
        for (int i = 0; i < 5; i++) { 
            account.draw(amount, i); 
        } 
    } 
 
}
package com.demo.test; 
 
public class DepositeMoneyThread extends Thread{ 
     
    private Account account; 
    private double amount; 
 
    public DepositeMoneyThread(String threadName, Account account, double amount) { 
        super(threadName); 
        this.account = account; 
        this.amount = amount; 
    } 
 
    public void run() { 
        for (int i = 0; i < 5; i++) { 
            account.deposite(amount, i); 
        } 
    } 
 
}
package com.demo.test; 
 
public class ThreadTest{ 
 
    public static void main(String[] args) { 
        Account account = new Account("123456", 0); 
 
        Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700); 
        Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700); 
 
        drawMoneyThread.start(); 
        depositeMoneyThread.start(); 
    } 
 
}

运行结果:

取钱线程 开始要执行wait操作 -- i=0 
存钱线程 存款:700.0 -- i=0 
存钱线程-- 存钱 -- 执行完毕 -- i=0 
存钱线程 开始要执行wait操作 -- i=1 
取钱线程 执行了wait操作 -- i=0 
取钱线程 取钱:700.0 -- i=1 
取钱线程-- 取钱 -- 执行完毕 -- i=1 
取钱线程 开始要执行wait操作 -- i=2 
存钱线程 执行了wait操作 -- i=1 
存钱线程 存款:700.0 -- i=2 
存钱线程-- 存钱 -- 执行完毕 -- i=2 
存钱线程 开始要执行wait操作 -- i=3 
取钱线程 执行了wait操作 -- i=2 
取钱线程 取钱:700.0 -- i=3 
取钱线程-- 取钱 -- 执行完毕 -- i=3 
取钱线程 开始要执行wait操作 -- i=4 
存钱线程 执行了wait操作 -- i=3 
存钱线程 存款:700.0 -- i=4 
存钱线程-- 存钱 -- 执行完毕 -- i=4 
取钱线程 执行了wait操作 -- i=4

由此,我们需要注意如下几点:

1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续执行,直到当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,如果下面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于相同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。

七、线程优先级和守护线程

1、线程优先级

      java中的线程优先级的范围是1~10,默认的优先级是5。每个线程默认的优先级都与创建它的父线程具有相同的优先级。默认情况下,mian线程具有普通优先级。“高优先级线程”会优先于“低优先级线程”执行。Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回线程优先级。

Thread类有3个静态常量:

 MAX_PRIORITY = 10 
 
 MIN_PRIORITY = 1 
 
 NORM_PRIORITY = 5

2、守护线程

      java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。

看一个例子,主线程中建立一个守护线程,当主线程结束时,守护线程也跟着结束。

package com.demo.test; 
 
public class DaemonThreadTest { 
 
     public static void main(String[] args){ 
          
        Thread mainThread = new Thread(new Runnable(){ 
            @Override 
            public void run(){ 
                Thread childThread = new Thread(new ClildThread()); 
                childThread.setDaemon(true); 
                childThread.start(); 
                System.out.println("I'm main thread..."); 
            } 
        }); 
        mainThread.start(); 
    } 
}
package com.demo.test; 
 
import java.util.concurrent.TimeUnit; 
 
public class ClildThread implements Runnable{ 
 
    @Override 
    public void run(){ 
         
        while(true){ 
            System.out.println("I'm child thread.."); 
            try{ 
                TimeUnit.MILLISECONDS.sleep(1000); 
            } 
            catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
        } 
    } 
}

运行结果:

I'm main thread... 
I'm child thread..

如果不指定childThread为守护线程,当主线程结束时,childThread还在继续运行,如下:

package com.demo.test; 
 
public class DaemonThreadTest { 
 
     public static void main(String[] args){ 
          
        Thread mainThread = new Thread(new Runnable(){ 
            @Override 
            public void run(){ 
                Thread childThread = new Thread(new ClildThread()); 
                childThread.setDaemon(false); 
                childThread.start(); 
                System.out.println("I'm main thread..."); 
            } 
        }); 
        mainThread.start(); 
    } 
}
package com.demo.test; 
 
import java.util.concurrent.TimeUnit; 
 
public class ClildThread implements Runnable{ 
 
    @Override 
    public void run(){ 
         
        while(true){ 
            System.out.println("I'm child thread.."); 
            try{ 
                TimeUnit.MILLISECONDS.sleep(1000); 
            } 
            catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
        } 
    } 
}

运行结果:

I'm main thread... 
I'm child thread.. 
I'm child thread.. 
I'm child thread.. 
I'm child thread.. 
I'm child thread..(无限输出)

可以看到,当主线程结束时,childThread是非守护线程,就会无限的执行。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 

(2) 在Daemon线程中产生的新线程也是Daemon的。

(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/17244.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论