zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java进程和线程

2023-09-27 14:29:04 时间

引用博客园一位牛人的文章,原文:http://www.cnblogs.com/lwbqqyumidi/p/3821389.html

1. 进程和线程
进程:进程表示一个运行的程序,程序的代码段,数据段这些都是存放在磁盘中的,在运行时加载到内存中。

进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器(CPU)赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。

线程:进程内部的一条执行路径或者一个控制单元。
两者的区别:
一个进程至少有一个线程
进程在执行过程中拥有独立的内存单元,而多个线程共享内存
2. 操作系统中线程和进程的概念
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。

线程是指进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。同时执行是人的感觉,在线程之间实际上轮换执行。

 

一、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动(静态程序代码的一次动态执行过程),是系统进行资源分配和调度的一个独立单位

二、线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和撤销另一个线程;

进程和线程的关系:

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体。

线程与进程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
(4)系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。

线程的划分尺度小于进程,使得多线程程序的并发性高。

另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序运行效率。

线程在执行过程中,每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,有应用程序提供多个线程执行控制。

从逻辑角度看,多线程的意义子啊与一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

 

多线程:指的是这个程序(一个进程)运行时产生了不止一个线程

并行与并发:

  • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
并发与并行
  • 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
    void transferMoney(User from, User to, float amount){
      to.setMoney(to.getBalance() + amount);
      from.setMoney(from.getBalance() - amount);
    }
  • 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

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多线程的创建及启动

Java中线程的创建常见有如三种基本形式

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

package cn.czbk.no12;

public class ThreadTest extends Thread{
    public void run(){
        for(int i=0;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+"当前执行第"+i+"次");
        }
    }
    public static void main(String[] args) {
        /**
         * 这一整个静态代码看成是一个程序,当执行的时候就被叫做进程,进程拥有系统分配给他的资源
         * 多线程:这个程序在运行时(进程)产生了不止一个线程
         * 在这个进程下由主线程main,t1,t2一共三个线程
         * 通过调用线程的start方法使线程进入到就绪状态
         * 但是CPU可能不会立即执行该线程,取决于调度算法和调度时机
         * 通过下面的实例也可以知道t1,t2是交叉执行的
         */
        ThreadTest t1=new ThreadTest();//创建t1使线程进入到新建状态
        ThreadTest t2=new ThreadTest();//创建t2使线程进入到新建状态
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"当前执行第"+i+"次");
            if(i==30){
                t1.start();//使t1进入到就绪状态,等待cpu的调度
                t2.start();//使t2进入到就绪状态,等待cpu的调度
            }
        }
    }
}
View Code

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

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

package cn.czbk.no12;

public class RunnTest implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName());
        }
        
    }
    public static void main(String[] args) {
        /**
         * 处理器先执行main进程
         * 当i=30时有可能会执行t1或t2有可能40+,50+之后在执行也有可能执行完100次之后在执行
         * 取决于处理器的调度时机
         */
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==30){
                RunnTest r1=new RunnTest();
                /**
                 * Thread类中实现的也是Runnable接口
                 * Thread类中的run()就是重写的Runnable接口中的run()
                 * 直接继承Thread的话就相当与间接的实现了Runnable的run()
                 * 在自己定义的类中实现Runnable接口的话还是没有和Thread(线程)直接扯上关系
                 * 所以要将你做好的实现的Runnable接口的类作为入参放到Thread的构造方法中
                 */
                Thread t1=new Thread(r1);
                Thread t2=new Thread(r1);
                t1.start();
                t2.start();
                
            }
        }
    }

}
View Code

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

package cn.czbk.no12;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) {
        MyCallable mc=new MyCallable();
        FutureTask<Integer> ft=new FutureTask<>(mc);
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==30){
                Thread t=new Thread(ft);
                t.start();
            }
        }
    }
}
class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        // TODO Auto-generated method stub
        int sum=0;
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            sum=sum+1;
        }
        return sum;
    }
    
}
View Code

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

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

三. Java多线程的就绪、运行和死亡状态

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。如:

package cn.czbk.no12;

public class ThreadStop extends Thread{
    private boolean stopflg=false;
    public void stopThread(){
        this.stopflg=true;
    }
    public void run(){
        for(int i=0;i<100 && !stopflg;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
    public static void main(String[] args) {
        //停掉一个单线程
        ThreadStop ts=new ThreadStop();
        for(int i=0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==30){
                ts.start();
            }
            if(i==40){
                ts.stopThread();
            }
        }
    }
}
View Code

四.Java多线程的阻塞状态与线程控制

1.join()

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

package cn.czbk.no12;

public class JoinTest implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
    public static void main(String[] args) {
        /**
         * A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行
         * 在main中调用t线程的join方法,则main被阻塞,直到t线程全部执行完毕。
         */
        JoinTest jt=new JoinTest();
        Thread t=new Thread(jt);
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==44){
                t.start();
                try {
                    t.join();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    
}
View Code

2.sleep()

sleep —— 让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,出于sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程执行。

前面有讲到,当调用了新建的线程的start()方法后,线程进入到就绪状态,可能会在接下来的某个时间获取CPU时间片得以执行,如果希望这个新线程必然性的立即执行,直接调用原来线程的sleep(1)即可。

package cn.czbk.no12;

public class SleepTest implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
    public static void main(String[] args) {
        /**
         * sleep —— 让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。
         * 在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。
         * 即使此时系统中没有任何其他可执行的线程,出于sleep()中的线程也不会执行。
         * 因此sleep()方法常用来暂停线程执行。
         * 当调用了新建的线程的start()方法后,线程进入到就绪状态,
         * 可能会在接下来的某个时间获取CPU时间片得以执行,如果希望这个新线程必然性的立即执行,直接调用原来线程的sleep(1)即可。 
         */
        JoinTest jt=new JoinTest();
        Thread t=new Thread(jt);
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==30){
                //使线程进入到就绪状态但是并不确定什么时候执行
                t.start();
                try {
                    //让当前执行的线程休眠1毫秒,当前的线程指的是main
                    //因为CPU不会空闲下来,main休眠后就会执行另外的线程t
                    //等1毫秒过后才会放到就绪队列中等待cpu调用
                    t.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    
}
View Code

3.后台线程(Daemon Thread)

概念/目的:后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。

生命周期:后台线程的生命周期与前台线程生命周期有一定关联。主要体现在:当所有的前台线程都进入死亡状态时,后台线程会自动死亡(其实这个也很好理解,因为后台线程存在的目的在于为前台线程服务的,既然所有的前台线程都死亡了,那它自己还留着有什么用...伟大啊 ! !)。

设置后台线程:调用Thread对象的setDaemon(true)方法可以将指定的线程设置为后台线程。

package cn.czbk.no12;

public class HouTaiThread extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        /**
         * 后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。
         * 如JVM中的垃圾回收线程。
         * 生命周期:后台线程的生命周期与前台线程生命周期有一定关联。
         * 主要体现在:当所有的前台线程都进入死亡状态时,后台线程会自动死亡(没有服务的对象了)
         * main前台线程,ht后台线程
         */
        HouTaiThread ht=new HouTaiThread();
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==20){
                ht.setDaemon(true);//将该线程设置为后台线程
                ht.start();
            }
        }
    }
}
View Code

4.改变线程的优先级/setPriority():

每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。

设置线程优先级:setPriority(int priorityLevel)。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5

获取线程优先级:getPriority()。

注:具有较高线程优先级的线程对象仅表示此线程具有较多的执行机会,而非优先执行。

package cn.czbk.no12;

public class PriorityTest extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
            PriorityTest pt=new PriorityTest();
            for(int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName());
                if(i==1){
                    /**
                     * 设置线程优先级:setPriority(int priorityLevel)。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:
                     * MAX_PRIORITY:10
                     * MIN_PRIORITY:1
                     * NORM_PRIORITY:5
                     * 获取线程优先级:getPriority()。
                     * 注:具有较高线程优先级的线程对象仅表示此线程具有较多的执行机会,而非优先执行。
                     */
                    pt.setPriority(MAX_PRIORITY);
                    pt.start();
                }
            }
    }
}
View Code

5.线程让步:yield()

yield()方法与线程优先级有关,当某个线程调用yiled()方法从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。

package cn.czbk.no12;

public class YieldTest implements Runnable{
    
    public static void main(String[] args) {
        /**
         *  运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
         *  当调用线程的yield()方法时,线程从运行状态转换为就绪状态,
         *  但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,
         *  因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
         *  当某个线程调用yiled()方法从运行状态转换到就绪状态后,
         *  CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。
         */
        YieldTest yt=new YieldTest();
        Thread t=new Thread(yt);
        TThread tt=new TThread();
        t.setPriority(Thread.MAX_PRIORITY);
        tt.setPriority(Thread.MIN_PRIORITY);
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i==20){
                t.start();
                tt.start();
                //线程让步,谁调用了yield()谁就回退到线程就绪状态
                TThread.yield();
            }
        }
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            System.out.println("Thread--1 "+i);
        }
    }
}
class TThread extends Thread{
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            System.out.println("Thread--2 "+i);
        }
    }
}
View Code

 

举例

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

package cn.czbk.no12;

public class ATMTError {
    
    public static void main(String[] args) {
        Account acc=new Account("999999", 1000);
        GetMoneyRunnable gmr=new GetMoneyRunnable(acc, 600);
        Thread t1=new Thread(gmr);
        Thread t2=new Thread(gmr);
        t1.start();
        t2.start();
    }
}
class GetMoneyRunnable implements Runnable{
    private Account account;
    private float getMoney;
    private float newBalance;
    public GetMoneyRunnable(Account acc,float getMoney) {
        // TODO Auto-generated constructor stub
        this.account=acc;
        this.getMoney=getMoney;
    }
    @Override
    public synchronized void run() {
        // TODO Auto-generated method stub
        /**
         * CPU会根据算法进行线程之间的切换不是一次性把run里面的方法执行完毕在开始下一个线程
         * 如在线程t1执行到xx?时CPU切换到t2线程中的run方法此时
         * 同样执行到这个判断但是因为余额还没有被更新所以也是判断成功的
         * 类似与Oracle中的事务
         */
        if(account.getBalance()>this.getMoney){//xx?
            System.out.println("取钱成功 取出:"+this.getMoney);
            newBalance=account.getBalance()-this.getMoney;
            account.setBalance(newBalance);
            System.out.println("账户余额:"+newBalance);
        }
    }
    
}
class Account{
    private String accNo;
    private float balance;
    public Account(String accNo,float balance){
        this.accNo=accNo;
        this.balance=balance;
    }
    public String getAccNo() {
        return accNo;
    }
    public void setAccNo(String accNo) {
        this.accNo = accNo;
    }
    public float getBalance() {
        return balance;
    }
    public void setBalance(float balance) {
        this.balance = balance;
    }
    
    
}
View Code

 

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

二.同步方法

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

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

1 public synchronized void run() {
2        
3     // ....
4  
5 }

 

三.同步代码块

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

同步代码块的格式为:

1 synchronized (obj) {
2             
3     //...
4 
5 }

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

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

 

四.Lock对象同步锁

上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。

使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

复制代码
 1 class X {
 2     
 3     // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
 4     private final Lock lock = new ReentrantLock();
 5     
 6     public void m(){
 7         // 加锁
 8         lock.lock();
 9         
10         //...  需要进行线程安全同步的代码
11         
12         // 释放Lock锁
13         lock.unlock();
14     }
15     
16 }

 

 五.wait()/notify()/notifyAll()线程通信

在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。

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

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

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

package cn.czbk.no12;

public class ATMCorrect {
    /**
     * 联想那张图!!! 1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;
     * 2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;
     * 3.notify()/notifyAll()执行后,如果后面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;
     * 4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;
     * 5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
     * 
     * @param args
     */
    public static void main(String[] args) {
        BankAccount bankacc = new BankAccount("小明的账户", 1000);
        InAccount in = new InAccount("存钱进程", bankacc, 300);
        OutAccount out = new OutAccount("取钱进程", bankacc, 300);
        in.start();
        out.start();
    }
}

class InAccount extends Thread {
    private BankAccount bankaccount;
    private double amount;

    public InAccount(String threadName, BankAccount bankacc, double amount) {
        super(threadName);
        this.bankaccount = bankacc;
        this.amount = amount;
    }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            bankaccount.depositMoney(amount, i);
        }
    }
}

class OutAccount extends Thread {
    private BankAccount bankaccount;
    private double amount;

    public OutAccount(String threadName, BankAccount bankacc, double amount) {
        super(threadName);
        this.bankaccount = bankacc;
        this.amount = amount;
    }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            bankaccount.drawMoney(amount, i);
        }
    }
}

class BankAccount {
    private String bankacc;
    private double balance;
    private double newbalance;
    // 切换线程标识
    private boolean isAble = false;

    public BankAccount(String acc, double balance) {
        this.bankacc = acc;
        this.balance = balance;
    }

    /**
     * 两个线程 一个存钱线程 一个取钱线程 模拟先存钱后取钱交替往复的这一过程 让两个线程重复这种状态5次!!!
     */
    public synchronized void drawMoney(double drawMoney, int i) {
        if (isAble) {
            System.out.println("----------------------------开始取钱-----------------------------"
                    + Thread.currentThread().getName() + i);
            newbalance = balance - drawMoney;
            this.setBalance(newbalance);
            System.out.println("取钱成功,金额为:---" + drawMoney);
            System.out.println("取钱后账户余额为:-----" + this.balance);
            notifyAll();
            this.isAble = false;
            System.out.println("----------------------------取钱结束-----------------------------"
                    + Thread.currentThread().getName() + i);
        } else {
            System.out.println("----------------------------准备转换为存钱进程----------------------------"
                    + Thread.currentThread().getName() + i);
            try {
                // 一个线程执行wait()之后就进入到了阻塞队列中去了
                // 最后回重新进入到线程就绪状态
                // 当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
                wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }
    /**对共享资源进行访问的方法定义中加上synchronized关键字修饰,
     * 使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,
     * +++其锁对象为当前方法所在的            对象自身(将当前方法所在对象进行了锁定就用一把锁锁了整个对象)            ++++
     * 多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),
     * 只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁(其他线程才能操作刚才被释放掉的锁对象(对象)),以此类推...
     * @param amount
     * @param i
     */
    public synchronized void depositMoney(double amount, int i) {
        if (!isAble) {
            System.out.println(
                    "++++++++++++++++++++++++++开始存钱++++++++++++++++++++++++++" + Thread.currentThread().getName() + i);
            this.setBalance(this.getBalance() + amount);
            System.out.println("存入金额为:" + amount);
            System.out.println("账户余额为:" + this.getBalance());
            this.isAble = true;
            notifyAll();
            try {
                //在唤醒其他线程后
                //自己进入到阻塞状态(休眠1000毫秒),这个时候此线程还是拥有方法的同步锁
                //直到当前线程执行完毕后才会释放方法锁,其他线程才可以调用这个方法
                System.out.println("现在这个存钱线程进入到了休眠状态!但是它睡觉的时候还抱着这把锁,(CPU在这个线程睡着的时候并不会赶他走,让它继续驻留)知道10000ms后它睡醒了才释放掉锁,其他线程才能操作这个对象。。。");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(
                    "++++++++++++++++++++++++++存钱结束++++++++++++++++++++++++++" + Thread.currentThread().getName() + i);
        } else {
            System.out.println("----准备切换为取钱进程----" + Thread.currentThread().getName() + i);
            try {
                wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    public String getBankacc() {
        return bankacc;
    }

    public void setBankacc(String bankacc) {
        this.bankacc = bankacc;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

}
View Code