精华内容
下载资源
问答
  • 要解决多线程并发访问一个资源的安全问题,java中提供了同步机制(synchronized)来解决。有三种方式实现同步机制:同步代码块格式:synchronized(同步) {// 需要同步操作的代码。}同步同步一个对象,是一个...

    线程的同步

    当我们使用多线程访问同一资源的时候,且这多个线程中对资源有的写的操作,就容器出现线程安全问题。

    要解决多线程并发访问一个资源的安全问题,java中提供了同步机制(synchronized)来解决。

    有三种方式实现同步机制:

    同步代码块格式:

    synchronized(同步锁) {

    // 需要同步操作的代码。

    }

    同步锁

    同步锁是一个对象,是一个抽象的概念,可以想象成在对象上标记了一个锁。

    1. 锁对象可以是任意类型的。Object

    2.多个线程对象,要使用同一把锁。

    在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就拥有资格进入代码块中,其他线程只能在外面等待着。(Blocked阻塞状态)

    同步方法

    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队。

    public synchronized voidmethod() {//可能会产生线程安全问题的代码

    }

    对于非static方法,同步锁就是this

    对于static方法,我们使用当前方法所在类的字节码对象(类名.class)

    @Overridepublic voidrun() {

    System.out.println(RunnableImpl.class);//com.zhiyou100.thread.demo01.RunnableImpl@15db9742

    System.out.println("this ---->" + this);//先判断票是否存在

    System.out.println();while(true){

    saleTicket();

    }

    }/** 静态的同步方法

    * 锁对象

    * 不能是this

    * this是创建对象之后产生的,静态方法优先于对象的创建

    * 静态同步方法中的锁对象是本类的class属性--->class文件对象(反射)*/

    public static synchronized voidsaleTicket() {/*synchronized (RunnableImpl.class) {*/

    if (ticket > 0) {//提高卖票的体验感 ,让程序睡眠下

    try{

    Thread.sleep(10);

    }catch(InterruptedException e) {

    e.printStackTrace();

    }//票存在,卖出第ticket张票

    System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");

    ticket--;

    }/*}*/}

    Lock锁

    同步代码块/同步方法具有的功能,Lock都有,除此之外更强大,更能体现出面向对象特征。

    Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:

    - public void lock():加同步锁

    - public void unlock():释放同步锁。

    备注:锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。

    public class RunnableImpl implementsRunnable {//定义一个多线程共享的资源 票

    private int ticket = 100;//1. 在成员的位置创建一个ReentrankLock对象

    Lock Lock = newReentrantLock();//设置线程的任务:卖票 此时窗口--->线程

    @Overridepublic voidrun() {//先判断票是否存在

    while(true){//2. 在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁

    Lock.lock();if (ticket > 0) {//提高卖票的体验感 ,让程序睡眠下

    try{

    Thread.sleep(10);//票存在,卖出第ticket张票

    System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");

    ticket--;

    }catch(InterruptedException e) {

    e.printStackTrace();

    }finally{//无论程序出现异常,此时都会把锁释放掉//在finally语句块中一般用于资源的释放,关闭IO流,释放lock锁,关闭数据库连接等等//3.在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。

    Lock.unlock();

    }

    }

    }

    }

    }

    线程状态

    当线程被创建并启动之后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态

    线程状态导致状态发生条件

    NEW(新建)

    线程刚被创建,但是还没有启动,还没有调用start方法

    RUNNABLE(可运行)

    线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器

    BLOCKED(锁阻塞)

    当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程就进入到Runnable状态

    WAITING(无限等待)

    一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒

    TIMED_WAITING(计时等待)

    同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知。带有超时参数的常用方法有Thread.sleep(),Object.wait().

    TERMINATED(被终止)

    因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

    Timed Waitng在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态

    其实当调用了sleep方法之后,当前正在执行的线程就进入到了计时等待状态。

    实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。public class MyThread extendsThread {

    @Overridepublic voidrun() {for (int i = 1;i <= 100 ; i ++) {if (i % 10 == 0) {

    System.out.println("------------------>" +i);

    }

    System.out.println(i);//在每个数字之间暂停1秒

    try{

    Thread.sleep(1000);

    }catch(Exception e) {

    e.printStackTrace();

    }

    }

    }//准备一个main函数

    public static voidmain(String[] args) {newMyThread().start();

    }

    }

    1.进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系

    2.为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠

    3.sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行。

    Blocked锁阻塞状态

    Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态。

    Waiting无限等待状态

    Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

    一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法

    其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

    等待唤醒

    线程间通信

    多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据。

    多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。

    等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法有三个如下:

    wait():线程不再活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是唤醒通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中。

    notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等候就餐最久的顾客最先入座。

    notifyAll():释放所通知对象的wait set中的全部线程。

    哪怕只通知了一个等待线程,被通知的线程也不能立即回复执行,因为它当初中断的地方是在同步块内,而此刻他已经不持有锁了,所以他需要再次尝试着去获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。

    如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态

    否则,从wait set中出来,又进入set中,线程就从WAITING状态转变成BLOCKED状态。

    调用wait和notify方法的注意细节:

    wait方法与notify方法必须由同一个锁对象调用。因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

    wait方法与notify方法是属于Object类的方法的。因为,锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

    wait方法与notify方法必须要在同步代码块或者同步方法中使用。因为,必须通过锁对象调用这两个方法来实现等待与唤醒。

    线程池

    一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程对象的操作,无需反复创建线程而消耗过多的系统资源。

    合理利用线程池能够带来什么样的好处:

    降低资源消耗。减少了线程的创建与销毁的次数,每个工作线程都可以被重复利用,可执行多个任务。

    提高了响应速度。当任务到达时,任务可以不需要等到线程的创建就能立即执行。

    提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,死机的风险也就更高)。

    Executors有创建线程池的方法如下:

    public static ExecutorService newFixedThreadPool(int nThreads):返回的就是线程池对象。(创建的是有界的线程池,也就是池中的线程个数可以指定最大数量)。

    获取到了一个线程池ExecutorService对象,在该类中定义了一个使用线程池对象的方法如下:

    public Future> submit(Runnable task):获取线程池中的某一个线程对象,并执行。

    使用线程池中线程对象的步骤:

    创建线程池对象

    创建Runnable接口子类对象。(task)

    提交Runnable接口子类对象。 (take task)

    关闭线程池(一般不做)。

    by-25

    展开全文
  • Java 线程同步

    2021-03-09 19:16:26
    线程安全问题关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几步骤。用户输入账户、密码,系统判断用户的账户、密码是否匹配。用户输入取款金额。系统判断账户余额...

    线程安全问题

    关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几个步骤。

    用户输入账户、密码,系统判断用户的账户、密码是否匹配。

    用户输入取款金额。

    系统判断账户余额是否大于取款金额。

    如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。

    乍一看上去,这个流程确实就是日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!

    按上面的流程去编写取款程序,并使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。此处忽略检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个实例变量。

    public classAccount{//封装账户编号、账户余额两个属性

    privateString accountNo;private doublebalance;publicAccount(){}//构造器

    public Account(String accountNo , doublebalance){this.accountNo =accountNo;this.balance =balance;

    }//省略getter、setter方法//下面两个方法根据accountNo来计算Account的hashCode和判断equals

    public inthashCode(){returnaccountNo.hashCode();

    }public booleanequals(Object obj){if (obj != null && obj.getClass() == Account.class){

    Account target=(Account)obj;returntarget.getAccountNo().equals(accountNo);

    }return false;

    }

    }

    接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。

    public class DrawThread extendsThread{//模拟用户账户

    privateAccount account;//当前取钱线程所希望取的钱数

    private doubledrawAmount;publicDrawThread(String name, Account account,doubledrawAmount){super(name);this.account =account;this.drawAmount =drawAmount;

    }//当多条线程修改同一个共享数据时,将涉及到数据安全问题。

    public voidrun(){//账户余额大于取钱数目

    if (account.getBalance() >=drawAmount){//吐出钞票

    System.out.println(getName() + "取钱成功!吐出钞票:" +drawAmount);/*try{

    Thread.sleep(1);

    }

    catch (InterruptedException ex){

    ex.printStackTrace();

    }*/

    //修改余额

    account.setBalance(account.getBalance() -drawAmount);

    System.out.println("\t余额为: " +account.getBalance());

    }else{

    System.out.println(getName()+ "取钱失败!余额不足!");

    }

    }

    }

    读者先不要管程序中那段被注释掉的粗体字代码,上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱。程序如下。

    public classTestDraw{public static voidmain(String[] args) {//创建一个账户

    Account acct = new Account("1234567" , 1000);//模拟两个线程对同一个账户取钱

    new DrawThread("甲" , acct , 800).start();new DrawThread("乙" , acct , 800).start();

    }

    }

    多次运行上面程序,很有可能都会看到如下图所示的错误结果。

    f030e23c4c3ef4055d9a6ca1d50eccf4.png

    运行结果并不是银行所期望的结果(不过有可能看到运行正确的效果),这正是多线程编程突然出现的“偶然”错误——因为线程调度的不确定性。假设系统线程调度器在粗体字代码处暂停,让另一个线程执行——为了强制暂停,只要取消上面程序中粗体字代码的注释即可。取消注释后再次编译 DrawThread.java,并再次运行 DrawTest 类,将总可以看到如上图所示的错误结果。

    问题出现了:账户余额只有1000时取出了 1600,而且账户余额出现了负值,这不是银行希望的结果。虽然上面程序是人为地使用 Thread.sleep(1) 来强制线程调度切换,但这种切换也是完全可能发生的——100000次操作只要有1次出现了错误,那就是编程错误引起的。

    同步代码块

    之所以出现如上图所示的结果,是因为 run() 方法的方法体不具有同步安全性——程序中有两个并发线程在修改 Account 对象;而且系统恰好在粗体字代码处执行线程切换,切换给另一个修改 Account 对象的线程,所以就出现了问题。

    提示:就像前面介绍的文件并发访问,当有两个进程并发修改同一个文件时就有可能造成异常。

    为了解决这个问题, Java 的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

    synchronized(obj){//此处的代码就是同步代码块}

    上面语法格式中 synchronized 后括号里的 obj 就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

    注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

    虽然 Java 程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,应该考虑使用账户(account )作为同步监视器,把程序修改成如下形式

    public class DrawThread extendsThread{//模拟用户账户

    privateAccount account;//当前取钱线程所希望取的钱数

    private doubledrawAmount;publicDrawThread(String name, Account account,doubledrawAmount){super(name);this.account =account;this.drawAmount =drawAmount;

    }//当多条线程修改同一个共享数据时,将涉及到数据安全问题。

    public voidrun(){//使用account作为同步监视器,任何线程进入下面同步代码块之前,//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它//这种做法符合:加锁-->修改完成-->释放锁 逻辑

    synchronized(account){//账户余额大于取钱数目

    if (account.getBalance() >=drawAmount) {//吐出钞票

    System.out.println(getName() +

    "取钱成功!吐出钞票:" +drawAmount);try{

    Thread.sleep(1);

    }catch(InterruptedException ex){

    ex.printStackTrace();

    }//修改余额

    account.setBalance(account.getBalance() -drawAmount);

    System.out.println("\t余额为: " +account.getBalance());

    }else{

    System.out.println(getName()+ "取钱失败!余额不足!");

    }

    }

    }

    }

    上面程序使用 synchronized 将 run() 方法里的方法体修改成同步代码块,该同步代码块的同步监视器是 account 对象,这样的做法符合“加锁一修改一释放锁”的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

    将 DrawThread 修改为上面所示的情形之后,多次运行该程序,总可以看到如下图所示的正确结果。

    5e389a2f580aab66a8dc9effcc7de6fe.png

    同步方法

    与同步代码块对应, Java 的多线程安全支持还提供了同步方法,同步方法就是使用 synchronized 关键字来修饰某个方法,则该方法称为同步方法。对于 synchronized 修饰的实例方法(非 static 方法)而言,无须显式指定同步监视器,同步方法的同步监视器是 this ,也就是调用该方法的对象。

    通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征。

    该类的对象可以被多个线程安全地访问。

    每个线程调用该对象的任意方法之后都将得到正确结果。

    每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

    前面介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。例如上面的 Account 就是一个可变类,它的 accountNo 和 balance 两个成员变量都可以被改变,当两个线程同时修改 Account 对象的 balance 成员变量的值时,程序就出现了异常。下面将 Account 类对 balance 的访问设置成线程安全的,那么只要把修改 balance 的方法变成同步方法即可。程序如下所示

    public classAccount {//封装账户编号、账户余额的两个成员变量

    privateString accountNo;private doublebalance;//构造器

    public Account(String accountNo, doublebalance) {this.accountNo =accountNo;this.balance =balance;

    }public voidsetAccountNo(String accountNo) {this.accountNo =accountNo;

    }publicString getAccountNo() {return this.accountNo;

    }//因为账户余额不允许随便修改,所以只为balance提供getter方法

    public doublegetBalance() {return this.balance;

    }//提供一个线程安全的draw()方法来完成取钱操作

    public synchronized void draw(doubledrawAmount) {//账户余额大于取钱数目

    if (balance >=drawAmount) {//吐出钞票

    System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" +drawAmount);try{

    Thread.sleep(1);

    }catch(InterruptedException ex) {

    ex.printStackTrace();

    }//修改余额

    balance -=drawAmount;

    System.out.println("\t余额为: " +balance);

    }else{

    System.out.println(Thread.currentThread().getName()+ "取钱失败!余额不足!");

    }

    }public inthashCode() {returnaccountNo.hashCode();

    }public booleanequals(Object obj) {if (obj != null && obj.getClass() == Account.class) {

    Account target=(Account) obj;returntarget.getAccountNo().equals(accountNo);

    }return false;

    }

    }

    上面程序中增加了一个代表取钱的 draw() 方法,并使用了 synchronized 关键字修饰该方法,把该方法变成同步方法,该同步方法的同步监视器是 this ,因此对于同一个 Account 账户而言,任意时刻只能有一个线程获得对 Account 对象的锁定,然后进入 draw() 方法执行取钱操作——这样也可以保证多个线程并发取钱的线程安全。

    因为 Account 类中已经提供了 draw() 方法,而且取消了 setBalance() 方法, DrawThread 线程类需要改写,该线程类的 run() 方法只要调用 Account 对象的 draw() 方法即可执行取钱操作。 run() 方法代码片段如下。

    注意:synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。

    public voidrun(){

    account.draw(drawAmount);

    }

    上面的 DrawThread 类无须自己实现取钱操作,而是直接调用 account 的 draw() 方法来执行取钱操作。由于已经使用 synchronized 关键字修饰了 draw() 方法,同步方法的同步监视器是 this,而 this 总代表调用该方法的对象——在上面示例中,调用 draw() 方法的对象是 account ,因此多个线程并发修改同一份 account 之前,必须先对 account 对象加锁。这也符合了 “ 加锁——修改——释放锁 ” 的逻辑 。

    提示:在 Account 里定义 draw() 方法,而不是直接在 run() 方法中实现取钱逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式: Domain Driven Design (领域驱动设计, DDD ),这种方式认为每个类都应该是完备的领域对象,例如 Account 代表用户账户,应该提供用户账户的相关方法;通过 draw() 方法来执行取钱操作(实际上还应该提供 transfer() 等方法来完成转账等操作),而不是直接将  setBalance() 方法暴露出来任人操作,这样才可以更好地保证 Account 对象的完整性和一致性。

    可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略。

    不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面 Account 类中的 accountNo 实例变量就无须同步,所以程序只对 draw() 方法进行了同步控制。

    如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

    提示:JDK 所提供的 StringBuilder、StringBuffer 就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用 StringBuilder 来保证较好的性能;当需要保证多线程安全时,就应该使用 StringBuffer 。

    释放同步监视器的锁定

    任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。

    当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

    当前线程在同步代码块、同步方法中遇到 break 、 return 终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。

    当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。

    当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器。

    在如下所示的情况下,线程不会释放同步监视器。

    线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、 Thread.yield() 方法来暂停当前线程的执行,当前线程不会释放同步监视器。

    线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用 suspend() 和 resume() 方法来控制线程。

    同步锁(Lock)

    从 Java5 开始,Java 提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由 Lock 对象充当。

    Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作,Lock 允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的 Condition 对象。

    Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

    某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁),Lock、ReadWriteLock 是 Java5 提供的两个根接口,并为 Lock 提供了 ReentrantLock (可重入锁)实现类,为 ReadWriteLock 提供了 ReentrantReadWriteLock 实现类。

    Java8 新增了新型的 StampedLock 类,在大多数场景中它可以替代传统的 ReentrantReadWriteLock 。ReentrantReadWriteLock 为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。

    在实现线程安全的控制中,比较常用的是 ReentrantLock (可重入锁)。使用该 Lock 对象可以显式地加锁、释放锁,通常使用 ReentrantLock 的代码格式如下:

    classX{//定义锁对象

    private final ReentrantLock lock = newReentrantLock();//...//定义需要保证线程安全的方法

    public voidm(){//加锁lock.lock();try{//需要保证线程安全的代码//...method body}//使用finally块来保证释放锁

    finally{

    lock.unlock();

    }

    }

    }

    使用 ReentrantLock  对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用 finally 块来确保在必要时释放锁。通常使用 ReentrantLock 对象,可以把 Account 类改为如下形式,它依然是线程安全的。

    public classAccount{//定义锁对象

    private final ReentrantLock lock = newReentrantLock();privateString accountNo;private doublebalance;publicAccount(){}public Account(String accountNo , doublebalance){this.accountNo =accountNo;this.balance =balance;

    }public voidsetAccountNo(String accountNo){this.accountNo =accountNo;

    }publicString getAccountNo(){return this.accountNo;

    }public doublegetBalance(){return this.balance;

    }public void draw(doubledrawAmount){

    lock.lock();try{//账户余额大于取钱数目

    if (balance >=drawAmount){//吐出钞票

    System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" +drawAmount);try{

    Thread.sleep(1);

    }catch(InterruptedException ex){

    ex.printStackTrace();

    }//修改余额

    balance -=drawAmount;

    System.out.println("\t余额为: " +balance);

    }else{

    System.out.println(Thread.currentThread().getName()+

    "取钱失败!余额不足!");

    }

    }finally{

    lock.unlock();

    }

    }public inthashCode(){returnaccountNo.hashCode();

    }public booleanequals(Object obj){if (obj != null && obj.getClass() == Account.class){

    Account target=(Account)obj;returntarget.getAccountNo().equals(accountNo);

    }return false;

    }

    }

    上面程序中的第一行粗体字代码定义了一个 ReentrantLock 对象,程序中实现 draw() 方法时,进入方法开始执行后立即请求对 ReentrantLock 对象进行加锁,当执行完 draw() 方法的取钱逻辑之后,程序使用  finally 块来确保释放锁。

    提示:使用 Lock 与使用同步方法有点相似,只是使用 Lock 时显式使用 Lock 对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁——修改——释放锁”的操作模式,而且使用 Lock 对象时每个 Lock 对象对应一个 Account 对象, 一样可以保证对于同一个 Account 对象,同一时刻只能有一个线程能进入临界区。

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

    虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。 Lock 提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的 tryLock() 方法,以及试图获取可中断锁的 locklntermptibly() 方法,还有获取超时失效锁的 tryLock(long, TimeUnit) 方法。

    ReentrantLock 锁具有可重入性,也就是说,一个线程可以对已被加锁的 ReentrantLock 锁再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

    死锁

    当两个线程相互等待对方释放同步监视器时就会发生死锁, Java 虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现 。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

    死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。

    classA{public synchronized voidfoo(B b){

    System.out.println("当前线程名: " +Thread.currentThread().getName()+ " 进入了A实例的foo方法");try{

    Thread.sleep(200);

    }catch(InterruptedException ex){

    ex.printStackTrace();

    }

    System.out.println("当前线程名: " +Thread.currentThread().getName()+ " 企图调用B实例的last方法");

    b.last();

    }public synchronized voidlast(){

    System.out.println("进入了A类的last方法内部");

    }

    }classB{public synchronized voidbar(A a){

    System.out.println("当前线程名: "+ Thread.currentThread().getName() + " 进入了B实例的bar方法");try{

    Thread.sleep(200);

    }catch(InterruptedException ex){

    ex.printStackTrace();

    }

    System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法");

    a.last();

    }public synchronized voidlast(){

    System.out.println("进入了B类的last方法内部");

    }

    }public class DeadLock implementsRunnable{

    A a= newA();

    B b= newB();public voidinit(){

    Thread.currentThread().setName("主线程");//调用a对象的foo方法

    a.foo(b);

    System.out.println("进入了主线程之后");

    }public voidrun(){

    Thread.currentThread().setName("副线程");//调用b对象的bar方法

    b.bar(a);

    System.out.println("进入了副线程之后");

    }public static voidmain(String[] args){

    DeadLock dl= newDeadLock();//以dl为target启动新线程

    newThread(dl).start();//执行init方法作为新线程

    dl.init();

    }

    }

    运行上面的程序,将会看到如下图所示的效果。

    4b15612c3c2a1819e685494d170d3da0.png

    从上图可以看出,程序既无法向下执行,也不会抛出任何异常,一直“僵持”着,究其原因,是因为:上面的程序 A 对象和 B 对象的方法都是同步方法,也就是 A 对象和 B 对象都是同步锁。程序中两个线程执行,一个线程的执行体是 DeadLock 类的 run() 方法,另一个线程的线程执行体是 DeadLock 的 init() 方法(主线程调用了 init() 方法)。其中 run() 方法中让 B 对象调用 bar() 方法,而 init() 方法让 A 对象调用  foo() 方法。上图显示 init() 方法先执行,调用了 A 对象的 foo() 方法,进入 foo() 方法之前,该线程对 A 对象加锁——当程序执行到①号代码时,主线程暂停200ms;CPU 切换到执行另一个线程,让 B 对象执行  bar() 方法,所以看到副线程开始执行 B 实例的 bar() 方法,进入 bar() 方法之前,该线程对 B 对象加锁——当程序执行到②号代码时,副线程也暂停200ms;接下来主线程会先醒过来,继续向下执行,直到③号代码处希望调用 B 对象的 last() 方法——执行该方法之前必须先对 B 对象加锁,但此时副线程正保持着 B 对象的锁,所以主线程阻塞;接下来副线程应该也醒过来了,继续向下执行,直到④号代码处希望调用 A 对象的 last() 方法一一执行该方法之前必须先对 A 对象加锁,但此时主线程没有释放对 A 对象的锁——至此,就出现了主线程保持着 A 对象的锁,等待对 B 对象加锁,而副线程保持着 B 对象的锁,等待对 A 对象加锁,两个线程互相等待对方先释放,所以就出现了死锁。

    注意:由于 Thread 类的 suspend() 方法也很容易导致死锁,所以 Java 不再推荐使用该方法来暂停线程的执行。

    展开全文
  • 1线程与进程的区别?...线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量...

    1、线程与进程的区别?

    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

    线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

    进程是资源分配的最小单位,线程是程序执行的最小单位。

    进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

    线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

    但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

    2、线程的创建方式

    2.1继承Thread类

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

    (2)创建Thread子类的实例,即创建了线程对象。

    (3)调用线程对象的start()方法来启动该线程。

    package Thread;

    import java.util.concurrent.*;

    public class TestThread {

    public static void main(String[] args) throws Exception {

    testExtends();

    }

    public static void testExtends() throws Exception {

    Thread t1 = new MyThreadExtends();

    Thread t2 = new MyThreadExtends();

    t1.start();

    t2.start();

    }

    }

    class MyThreadExtends extends Thread {

    @Override

    public void run() {

    System.out.println("通过继承Thread,线程号:" + currentThread().getName());

    }

    }

    2.2实现Runnable接口

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

    (3)调用线程对象的start()方法来启动该线程。

    package Thread;

    import java.util.concurrent.*;

    //测试类

    public class TestThread {

    public static void main(String[] args) throws Exception {

    testImplents();

    }

    public static void testImplents() throws Exception {

    MyThreadImplements myThreadImplements = new MyThreadImplements();

    Thread t1 = new Thread(myThreadImplements);

    Thread t2 = new Thread(myThreadImplements, "my thread -2");

    t1.start();

    t2.start();

    }

    }

    //线程类

    class MyThreadImplements implements Runnable {

    @Override

    public void run() {

    System.out.println("通过实现Runable,线程号:" + Thread.currentThread().getName());

    }

    }

    2.3实现Callable接口

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    package Thread;

    import java.util.concurrent.*;

    public class TestThread {

    public static void main(String[] args) throws Exception {

    testCallable();

    }

    public static void testCallable() throws Exception {

    Callable callable = new MyThreadCallable();

    FutureTask task = new FutureTask(callable);

    new Thread(task).start();

    System.out.println(task.get());

    Thread.sleep(10);//等待线程执行结束

    //task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值

    //get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待

    System.out.println(task.get(100L, TimeUnit.MILLISECONDS));

    }

    }

    class MyThreadCallable implements Callable {

    @Override

    public Object call() throws Exception {

    System.out.println("通过实现Callable,线程号:" + Thread.currentThread().getName());

    return 10;

    }

    }

    2.4三种方式的优缺点

    采用继承Thread类方式:

    (1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。   (2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

    采用实现Runnable接口方式:

    (1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    (2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

    Runnable和Callable的区别:

    (1)Callable规定的方法是call(),Runnable规定的方法是run().   (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的   (3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常   (4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    start()和run()的区别:

    start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行

    run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

    3、线程的生命周期

    Java 线程 的 状态 可以 使用 监控 工具 查看, 也可以 通过 Thread. getState() 调用 来 获取。 Thread. getState() 的 返回 值 类型 Thread. State 是一 个 枚举 类型( Enum)。 Thread. State 所 定义 的 线程 状态 包括 以下 几种。

    NEW: 一个 已 创建 而未 启动 的 线程 处于 该 状态。 由于 一个 线程 实例 只 能够 被 启动 一次, 因此 一个 线程 只可 能有 一次 处于 该状态。

    RUNNABLE: 该 状态 可以 被 看成 一个 复合 状态。 它 包括 两个 子 状态: READY 和 RUNNING。 前者 表示 处于 该 状态 的 线程 可以 被 线程 调度 器( Scheduler) 进行 调度 而使 之 处于 RUNNING 状态。 后者 表示 处于 该 状态 的 线程 正在 运行, 即 相应 线程 对象 的 run 方法 所 对应 的 指令 正在 由 处理器 执行。 执行 Thread. yield() 的 线程, 其 状态 可能 会 由 RUNNING 转换 为 READY。 处于 READY 子 状态 的 线程 也 被称为 活跃 线程。

    BLOCKED: 一个 线程 发起 一个 阻塞 式 I/ O( Blocking I/ O) 操作 后, 或者申请 一个 由其 他 线程 持有 的 独占 资源( 比如 锁) 时, 相应 的 线程 会 处于 该 状态。 处于 BLOCKED 状态 的 线程 并不 会 占用 处理器 资源。 当 阻塞 式 I/ O 操作 完成 后, 或者 线程 获 得了 其 申请 的 资源, 该 线程 的 状态 又可 以 转换 为 RUNNABLE。

    WAITING: 一个 线程 执行 了 某些 特定 方法 之后 就会 处于 这种 等待 其他 线程 执行 另外 一些 特定 操作 的 状态。 能够 使其 执行 线程 变更 为 WAITING 状态 的 方法 包括: Object. wait()、 Thread. join() 和 LockSupport. park( Object)。 能够使 相应 线程 从 WAITING 变更 为 RUNNABLE 的 相应 方法 包括: Object. notify()/ notifyAll() 和 LockSupport. unpark( Object))。

    TIMED_ WAITING: 该 状态 和 WAITING 类似, 差别 在于 处于 该 状态 的 线程 并非 无限制 地 等待 其他 线程 执行 特定 操作, 而是 处于 带有 时间 限制 的 等待 状态。 当 其他 线程 没有 在 指定 时间 内 执行 该 线程 所 期望 的 特定 操作 时, 该 线程 的 状态 自动 转换 为 RUNNABLE。

    TERMINATED: 已经 执行 结束 的 线程 处于 该 状态。 由于 一个 线程 实例 只 能够 被 启动 一次, 因此 一个 线程 也 只可 能有 一次 处于 该 状态。 Thread. run() 正常 返回 或者 由于 抛出 异常 而 提前 终止 都会 导致 相应 线程 处于 该 状态。 一个 线程 在 其 整个 生命 周期 中, 只可 能有 一次 处于 NEW 状态 和 TERMINATED 状态。

    4、锁

    线程 同步 机制 是一 套用 于 协调 线程 间的 数据 访问( Data access) 及 活动( Activity) 的 机制, 该 机制 用于 保障 线程 安全 以及 实现 这些 线程 的 共同 目标。 如果 把 线程 比作 在 公路 上 行驶 的 车辆, 那么 线程 同步 机制 就 好比 是 任何 车辆 都 需要 遵循 的 交通规则。 公路 上 行驶 的 车辆 只有 遵守 交通规则 才能 够 达到 其 目的—— 安全 地 到达 目的地。从 广义 上 来说, Java 平台提供 的线程同步机制包括锁、volatile 关键字、 final 关键字、 static 关键字 以及 一些 相关 的 API, 如 Object. wait()/ Object. notify() 等。

    线程 安全 问题 的 产生 前提 是 多个 线程 并发 访问 共享 变量、 共享 资源( 以下 统称 为 共享 数据)。 于是, 我们 很容易 想到 一种 保障 线程 安全 的 方法—— 将 多个 线程 对 共享 数据 的 并发访问转换 为 串行 访问, 即 一个 共享 数据 一次 只能 被 一个 线程 访问, 该 线程 访问 结束 后 其他 线程 才能 对其 进行 访问。 锁( Lock) 就是 利用 这种 思路 以 保障 线程 安全 的 线程 同步 机制。

    一个 线程 在 访问 共享 数据 前 必须 申请 相应 的 锁( 许可证), 线程 的 这个 动作 被称为 锁 的 获得( Acquire),一个 锁 一次 只能 被 一个 线程 持有。 锁 的 持有 线程 可 以对 该 锁 所 保护 的 共享 数据 进行 访问, 访问 结束 后 该 线程 必须 释放( Release) 相应 的 锁。 锁 的 持有 线程 在 其 获得 锁 之后 和 释放 锁 之前 这段 时 间内 所 执行 的 代码 被称为 临界区( Critical Section)。 因此, 共享 数据 只 允许 在 临界 区内 进行 访问, 临界 区 一次 只能 被 一个 线程 执行。如果 有多 个 线程 访问 同一个 锁 所 保护 的 共享 数据, 那么 我们 就 称 这些 线程 同步 在这 个 锁上。

    4.1与锁相关的概念

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    有序性:即程序执行的顺序按照代码的先后顺序执行。处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

    可 重入 性( Reentrancy): 一个 线程 在 其 持有 一个 锁 的 时候 能否 再次( 或者 多次) 申请 该 锁。 如果 一个 线程 持有 一个 锁 的 时候 还能 够 继续 成功 申请 该 锁, 那么 我们 就 称 该 锁 是 可 重入 的( Reentrant), 否则 我们 就 称 该 锁 为 非可 重入 的( Non- reentrant)。

    资源 的 争用、 调度:锁 可以 被 看作 多 线程 程序 访问 共享 数据 时 所需 持有 的 一种 排他性 资源。 锁 的 调度基本上 是由 Java 虚拟 机 来决定的, Java 平 台中 锁 的 调度 策略包括 公平策略 和 非公平 策略, 相应 的 锁 就被 称为 公平 锁 和 非 公平 锁。 内部 锁 属于非 公平 锁, 而 显 式 锁 则 既 支持 公平 锁 又 支持 非 公平 锁。

    锁 的 粒度:一个 锁 实例 可以 保护 一个 或者 多个 共享 数据。 一个 锁 实例 所 保护 的 共享 数据 的 数量 大小 就被 称为 该 锁 的 粒度( Granularity)。 一个 锁 实例 保护 的 共享 数据 的 数量 大, 我们 就 称 该 锁 的 粒度 粗, 否则 就 称 该 锁 的 粒度 细,锁 粒度 的 粗细 是 相对 的。 锁 的 粒 度过 粗 会 导致 线程 在 申请 锁 的 时候 需要 进行 不必 要的 等待。 这 好比 我们 去 银行 柜台 办理 业务 的 情形: 假如 一个 柜台 同时 能够 办理 多种 业务, 那么 就可能 出现 这样 的 场景—— 办理 客户 资料 变更 的 客户 需要 等待 前面 要 办理 定期 存款 的 客户。 而 如果 一个 柜台 只 办理 一种 业务, 比如 将 开户、 销 户 和 客户 资料 变更 归为 一种 业务 放在 一个 柜台 办理, 那么 办理 客户 资料 变更 的 客户 需要 等待 的 时间 就会 相对 减少。 不过, 锁 的 粒度 过细 会 增加 锁 调度 的 开销。

    4.2内部锁(sychronized关键字)

    内部 锁 是一 种 排他 锁, 它 能够保障 原子 性、 可见 性 和 有序 性。 内部 锁 是 通过 synchronized 关键字 实现 的。

    synchronized 实现 的 锁 就被称为 内部 锁,线程 对 内部 锁 的 申请 与 释放 的 动作 由 Java 虚拟 机 负责 代为 实施。内部 锁 的 使用 并不 会 导致 锁 泄漏。 这是 因为 Java 编译器( javac) 在 将 同步 块 代码 编译 为 字节 码 的 时候, 对 临界 区 中 可能 抛出 的 而 程序 代码 中 又 未 捕获 的 异常 进行 了 特殊( 代为) 处理, 这使 得 临界 区 的 代码 即使 抛出 异常 也不 会 妨碍 内部 锁 的 释放。

    内部锁的调度:Java 虚拟 机会 为 每个 内部 锁 分配 一个 入口 集( Entry Set), 用于 记录 等待 获得 相应 内部 锁 的 线程。 多个 线程 申请 同一个 锁 的 时候, 只有 一个 申请 者 能够 成为 该 锁 的 持有 线程( 即 申请 锁 的 操作 成功), 而 其他 申 请者 的 申请 操作 会 失败。 这些 申请 失败 的 线程 并不 会 抛出 异常, 而是 会被 暂停( 生命 周期 状态变为 BLOCKED) 并被 存入 相应 锁 的 入口 集中 等待 再次 申请 锁 的 机会。 入口 集中 的 线程 就被 称为 相应 内部 锁 的 等待 线程。 当 这些 线程 申请 的 锁 被 其 持有 线程 释放 的 时候, 该 锁 的 入口 集中 的 一个 任意 线程 会被 Java 虚拟 机 唤醒, 从而 得到 再次 申请 锁 的 机会。 由于 Java 虚拟 机 对 内部 锁 的 调度 仅 支持 非 公平 调度, 被 唤醒 的 等待 线程 占用 处理器 运行时 可能 还有 其他 新的 活跃 线程( 处于 RUNNABLE 状态, 且 未 进入 过 入口 集) 与 该 线程 抢占 这个 被 释放 锁, 因此 被 唤醒 的 线程 不一定 就能 成为 该 锁 的 持有 线程。 另外, Java 虚拟 机 如何 从 一个 锁 的 入口 集中 选择 一个 等待 线程, 作为 下一个 可以 参与 再次 申请 相应 锁 的 线程, 这个 细节 与 Java 虚拟 机 的 具体 实现 有关: 这个 被选 中的 线程 有可能 是 入口 集中 等待 时间 最长 的 线程, 也可能 是 等待 时间 最短 的 线程, 或者 完全 是 随机 的 一个 线程。 因此, 我们 不能 依赖 这个 具体 的 选择 算法。

    4.3显示锁:Lock接口

    显 式 锁 是 自 JDK 1. 5 开始 引入 的 排他 锁。 作为 一种 线程 同步 机制, 其 作用 与 内部 锁 相同。 它 提供 了 一些 内部 锁 所不 具备 的 特性, 但 并不是 内部 锁 的 替代品。

    显 式 锁( Explicit Lock) 是 java. util. concurrent. lcoks. Lock 接口 的 实例, 该 接口 对 显 式 锁 进行 了 抽象。

    显 式 锁 的 使用 方法如下所示。

    private final Lock LOCK=....//创建一个Lock接口实例

    ......

    lock.lock();//申请锁lock

    try{

    //在此对共享数据进行访问

    ......

    }finally{

    //总是在finally块中释放锁,以避免锁泄露

    lock.unlock();//释放锁lock

    }

    虽然 释放 锁 的操作通过 调用 Lock. unlock() 即可 实现, 但是 为了 避免 锁 泄漏, 我们 必须 将 这个 调用 放在 finally 块 中 执行。 这样, 无论是 临界 区 代码 执行 正常 结束 还是 由于 其 抛出 异常 而 提前 退出, 相应 锁 的 unlock 方法 总是 可以 被 执行, 从而 避免 了 锁 泄漏。 可见, 显 式 锁 不像 内部 锁 那样 可以由 编译器 代为 规避 锁 泄漏 问题。

    公平 锁 适合于 锁 被 持有 的 时间 相对 长 或者 线程 申请 锁 的 平均 间隔 时间 相对 长的 情形。遇到此情形时选用显示锁更为合理。总的来说公平锁的开销要比非公平锁大,因此,显示锁默认是采用非公平调度策略的。

    注意:内部锁是基于代码块的锁,而显示锁是基于对象的锁(充分发挥面向对象编程的灵活性)。

    4.4轻量级锁(volatile关键字)

    volatile 关键字 用于 修饰 共享 可变 变量, 即 没有 使用 final 关键字 修饰 的 实例 变量 或 静态 变量, 相应 的 变量 就被 称为 volatile 变量,

    private volatile int num;

    volatile 关键字 表示 被 修饰 的 变量 的 值 容易 变化( 即被 其他 线程 更改), 因而 不稳定。 volatile 变量 的 不稳定 性 意味着 对这 种 变量 的 读 和 写 操作 都 必须 从高 速 缓存 或者 主 内存( 也是 通过 高速 缓存 读取) 中 读取, 以 读取 变量 的 相对 新 值。 因此, volatile 变量 不 会被 编译器 分配 到 寄存器 进行 存储, 对 volatile 变量 的 读写 操作 都是 内存 访问( 访问 高速 缓存 相当于主内存) 操作。即volatile关键字保证了共享变量的可见性,只要共享变量一发生改变,就会使得其他线程工作内存中的变量无效,并将改变后的值更新到主存中,其他线程会到主存中去更新该共享变量的值。

    volatile 关键字 常被 称为 轻量级 锁, 其 作用 与 锁 的 作用 有 相同 的 地方: 保证 可见 性 和 有序 性。 所 不同 的 是, 在 原子 性 方面 它 仅能 保障 写 volatile 变量 操作 的 原子 性, 但 没有 锁 的 排他性; 其次, volatile 关键字 的 使用 不会 引起 上下文 切换( 这是 volatile 被 冠以“ 轻量级” 的 原因)。 因此, volatile 更 像是 一个 轻量级简易( 功能 比 锁 有限) 锁。

    展开全文
  • 这里银行的窗口就可以看做共享的资源,它每次只能接待一个顾客,而不同的顾客则可以看做是多个线程,他们都需要办理业务,但是又必须遵守先来后到的原则,排队等待前面的顾客办理完业务才能轮到自己独占窗口办理.....

    假设这样一个情景:在银行的营业厅内先后进来3个人,他们都要进行存款,若是只有一个营业窗口的话,通常的情况是每人都需要先领取顺序条,然后按序排队办理业务,而营业厅会根据号码的顺序依次叫号来处理顾客的问题。

    在这里银行的窗口就可以看做共享的资源,它每次只能接待一个顾客,而不同的顾客则可以看做是多个线程,他们都需要办理业务,但是又必须遵守先来后到的原则,排队等待前面的顾客办理完业务才能轮到自己独占窗口办理自己的业务(当然也可能存在插队现象,后面会讲到)。

    为了简化银行处理业务的过程,我们假设每个客户只是办理简单的存款业务,银行窗口只需清点出存款数额就代表业务处理完成。

    (1)首先我们定义一个银行类Bank,用它来模拟银行。为了能够让整个过程可视化,我们让这个类继承自Applet。而这个银行的主业务--存款(其实就是点钱),我们用方法countMoney来模拟,由于这是个临界资源,所以我们要给他加上互斥锁synchronize,以保证每次只有一个顾客能够使用这个功能。

    public class Bank extendsApplet{

    ......public synchronized void countMoney(String name, inttotal){int count = 1;while(count <=total){

    t.setText("(" + name + ") " + Integer.toString(count++));

    }

    notifyAll();

    }

    ......

    }

    为了能够看清楚这个排队等待数钱的过程,我们需要两个文本区域,一个用来显示当前正在办理业务的客户数钱的过程,另一个显示等待数钱的客户队列。

    public TextField counting= new TextField(10);public TextField deque = new TextField(100);

    另外我们给该银行模拟了三个用户,他们分别是马云、雷军和马化腾--。

    private MaYun maYun = null;private LeiJun leiJun = null;private MaHuaTeng maHuaTeng = null;

    整个Bank类如下所示,其中方法setDeque表示向排队队列中加入顾客。

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    public class Bank extendsApplet{public TextField counting = new TextField(10);public TextField deque = new TextField(100);private MaYun maYun = null;private LeiJun leiJun = null;private MaHuaTeng maHuaTeng = null;public voidinit() {

    add(counting);

    add(deque);

    maYun= new MaYun(Bank.this);

    leiJun= new LeiJun(Bank.this);

    maHuaTeng= new MaHuaTeng(Bank.this);

    }public voidsetDequeue(String person){

    String list=deque.getText();

    list= list + person + "->";

    deque.setText(list);

    }public synchronized void countMoney(String name, inttotal){int count = 1;while(count <=total){

    counting.setText("(" + name + ") " + Integer.toString(count++));

    }

    notifyAll();

    }public static void main(intargc, String args[]){

    Bank applet= newBank();

    Frame aFrame= new Frame("Counter2");

    aFrame.add(applet, BorderLayout.CENTER);

    aFrame.setSize(300, 200);

    applet.init();

    applet.start();

    aFrame.setVisible(true);

    }

    }

    View Code

    (2)定义三个线程类,在每个线程类的run方法中都调用银行类中的countMoney方法。由于countMoney加了互斥锁,所以每次只有一个线程可以执行这个方法,而其他的线程都处于阻塞状态。一旦一个县城执行完了countMoney方法,他就会调用notifyAll方法,唤醒其他阻塞的线程。这个过程就模仿了顾客在营业厅排队办理业务的场景。

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    public class MaYun extendsThread {private static int money = 10000;private static String name = "马云";

    Bank c= null;

    Button start= newButton(name);publicMaYun(Bank c){this.c =c;

    start.addActionListener(newStartListener());

    c.add(start);

    }public voidrun() {

    c.setDequeue(name);

    c.countMoney(name, money);

    }public class StartListener implementsActionListener{public voidactionPerformed(ActionEvent e) {

    MaYun.this.start();

    }

    };

    }

    View Code

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    public class LeiJun extendsThread {private static int money = 8000;private static String name = "雷军";

    Bank c= null;

    Button start= newButton(name);publicLeiJun(Bank c){this.c =c;

    start.addActionListener(newStartListener());

    c.add(start);

    }public voidrun() {

    c.setDequeue(name);

    c.countMoney(name, money);

    }public class StartListener implementsActionListener{public voidactionPerformed(ActionEvent e) {

    LeiJun.this.start();

    }

    };

    }

    View Code

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    public class MaHuaTeng extendsThread {private static int money = 20000;private static String name = "马化腾";

    Bank c= null;

    Button start= newButton(name);publicMaHuaTeng(Bank c){this.c =c;

    start.addActionListener(newStartListener());

    c.add(start);

    }public voidrun() {

    c.setDequeue(name);

    c.countMoney(name, money);

    }public class StartListener implementsActionListener{public voidactionPerformed(ActionEvent e) {

    MaHuaTeng.this.start();

    }

    };

    }

    View Code

    (3)下面给出实验过程中运行的界面结果。我们依次点击马云、雷军、马化腾三个按钮,表示这3个人依次来银行办理业务。由于马云是最先来到的,所以他可以立即到窗口办理点钱业务。而由于处理业务的窗口只有一个,雷军和马化腾只能等马云结束,他们才能办理自己的业务。但是在实际的运行过程中,我们发现马云办理完业务后,紧接着办理业务的不是雷军而是马化腾。这主要是因为当马云办理完业务后,雷军和马化腾并没有按照先来后到的排队顺序来,马化腾插队了。这说明使用notifyAll方法,所有阻塞的线程在获得临近资源的机会上是均等的,谁先抢到,谁先用。

    c9f085335091c676d07ea6ad8d9daa71.png

    267bd66de01ef5a950cec16e19aa8065.png

    d0498c07801ad3106ca59f590acee120.png

    展开全文
  • Java线程基本使用

    2021-03-06 04:59:59
    、概念1.进程1.1进程:是一个正在进行的程序,每...1.3举例java VM:Java VM启动的时候会有一个进程java.exe,该进程至少有一个线程在负责java程序的运行,而且这个线程运行的代码存在于main方法,该线程称...
  • 原标题:多线程中元老级的:Synchronized详解多线程中元老级的:...在Java中,synchronized可能是我们最早接触的了,JDK1.5之前synchronized是一个重量级,相对于juc包中的Lock,synchronize...
  • 本文实例为大家分享了java个线程同时写一个文件的具体代码,供大家参考,具体内容如下1.多线程线程是程序执行流的最小单元。是进程一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只...
  • 介绍完如何创建进程以及线程了,那么我们接着来看一个实例:利用多线程模拟 3 窗口卖票第种方法:继承 Thread 类创建窗口类 TicketSellpackage com.ys.thread;public class TicketSell extends Thread{//定义...
  • 、简介1、什么是线程要说线程,就必须先说说进程,进程就是程序的运行时的一个实例。线程呢可以看作单独地占有CPU时间来执行相应的代码的。对早期的计算机(如DOS)而言,线程既是进程,进程既是进程,因为她是单...
  • 当多个线程访问一个对象时如果不考虑这些线程在执行时环境下的调度和交替执行,也不需要进行额外的同步,或者调用方进行任何其他的协调操作,调用这对象的行为都可以获得正确的结果,那么这对象就是线程安全的...
  • - 继上次我们学习了线程礼让yield 线程强制执行join 守护线程 线程优先级相关的知识 - 本节将会帮助你了解... - 并发 同步 阻塞 synchronized概念浅析 - synchronized四种实现形式的理解以及demo
  • java线程基本使用

    2021-03-08 15:46:42
    .概念1.进程1.1进程:是一个正在进行的程序,每...1.3举例java VM:Java VM启动的时候会有一个进程java.exe,该进程至少有一个线程在负责java程序的运行,而且这个线程运行的代码存在于main方法,该线程称之...
  • java线程

    2021-02-23 09:27:49
    线程状态迁移图 线程有状态:New,Runnable(Ready/Runing),TimedWaiting,Walting,Blocked,Teminated,线程不建议关闭,只需要正常结束。 sleep 当前线程睡眠,睡眠结束后,继续执行。 yield 当前线程让出执行时间,...
  • JAVA SE】第十六章 进程、线程、同步线程锁的简介
  • 1、当前线程的状态,所持有的是否释放持有线程不释放锁线程执行同步代码块或同步方法时,程序调用Thread.sleep(Long l)、Thread.yield()方法暂停当前线程的执行,不释放线程执行同步代码块时,其它线程...
  • Java内置一个互斥,这就是意味着最多只有一个线程能够获得该,当线程A尝试去获得线程B持有的内置时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。...
  • 文章目录前言、同步代码块解决线程安全问题二、同步方法解决线程安全问题三、Lock解决线程安全问题 前言 我们首先使用学习到的多线程知识,模拟电影票售票过程,引出线程安全问题。 /* 需求:某电影院目前...
  • Java线程和同步的理解、 进程与线程在谈论线程之前,我们先来看看什么叫进程,以及进程与线程的关系。进程我们windows操作系统打开任务管理器,可以看到有项是“进程”,里面列举出了用户目前正在运行的...
  • Java线程安全与机制

    2021-03-23 20:09:41
    、对象的发布与逸出发布(publish): 使对象能够当前作⽤域之外的代码使⽤逸出(escape) :当某个不应该发布的对象被发布了二、并发编程的三重要特性1.原子性 :2.可见性 :3.有序性 :三、解决多线程遇到的...
  • 线程:进程一个执行单元,负责进程的程序的运行,一个进程至少要有一个线程。多线程程序:一个进程是可以有多个线程的,这应用程序也可以称之为多线程程序。程序启动了多线程,有什么应用呢?可以实现多...
  • Java线程线程同步

    2021-03-14 15:32:11
    六、多线程线程,同步①概念:并行:指两或多个在时间同一时刻发生(同时发生)并发:指两或多事件同一时间段内发生具体概念:操作系统,安装了多程序,并发指的是在一段时间内宏观上有多程序同时...
  • Java中哪些操作会使线程释放资源

    千次阅读 2021-04-21 08:59:21
    指定时间内让当前正在执行的线程暂停执行,但不会释放“标志”。不推荐使用。 sleep()使当前线程进入阻塞状态,指定时间内不会执行。 2.wait()方法 其他线程调用对象的notify或notifyAll方法前,导致当前...
  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上,这样别人想拿这数据就会阻塞直到它拿到(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它...
  • 1.线程可以被sleep()中断,但这种中断属于阻塞线程线程仍然可以获得cpu的执行权,如果睡眠线程不是后台进程且当主线程执行完时jvm不会停止还会等到睡眠线程。当然如果睡眠被interrupted中断会抛出异常...
  • 当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块,使用Synchronized关键字来修饰,被称作同步代码块,其语法格式为: synchronized(lock){ 操作共享资源代码块 } 上面的代码, lock...
  • Synchronized 是 Java 的关键字, 是利用的机制来实现同步的. 是Java内置的机制, 是JVM层面的. JDK 1.6 以前... 所谓互斥, 就是指一个锁一只能一个线程持有 可见性 的获得会隐含地包含刷新处理器缓存
  • \quad当多个线程访问一个共同的资源时,会发生线程安全问题。假设有共享变量i=0,有两任务,任务1对该变量进行+1操作,任务2对该变量进行-1操作。假设线程t1对i进行+1操作,但还未来得及保存,此时i=0,时间片就...
  • Java中线程原理详解

    2021-03-01 06:20:30
    Java是少数的集中支持多线程的语言之,大多数的语言智能运行单独的一个程序块,无法同时运行不同的多程序块,Java的多线程机制弥补了这缺憾,它可以让不同的程序块一起运行,这样可以让程序运行更加顺畅,同时...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,564
精华内容 25,825
关键字:

java锁只能在同一个线程中使用码

java 订阅