Java多线程共享变量控制

1. 可见性

如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性。如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量

2. JMM(java内存模型)

多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作线程成为一个副本中,改动之后,再更新回主线程中变量内存。

两条规定:

a.线程对共享变量的所有操作必须在工作内存中进行,不能直接操作主内存

b.不同线程间不能访问彼此的工作内存中的变量,线程间变量值的传递都必须经过主内存

如果一个线程1对共享变量x的修改对线程2可见的话,需要经过下列步骤:

a.线程1将更改x后的值更新到主内存

b.主内存将更新后的x的值更新到线程2的工作内存中x的副本

所以,要实现共享变量的可见性必须保证下列两点:

a.线程对工作内存中副本的更改能够及时的更新到主内存上

b.其他线程能够及时的将主内存上共享变量的更新刷新到自己工作内存的该变量的副本上

Java中可以通过synchronized、volatile、java concurrent类来实现共享变量的可见性

3. synchronized实现可见性

synchronized 实际上是对访问修改共享变量的代码块进行加互斥锁,多个线程对synchronized代码块的访问时,某一时刻仅仅有一个线程在访问和修改代码块中的内 容(加锁),其他所有的线程等待该线程离开代码块时(释放锁)才有机会进入synchronized代码块。

所以某一个线程进入synchronized代码块前后,执行过程入如下:

a.线程获得互斥锁

b.清空工作内存

c.从主内存拷贝共享变量最新的值到工作内存成为副本

d.执行代码

e.将修改后的副本的值刷新回主内存中

f.线程释放锁

随后,其他代码在进入synchronized代码块的时候,所读取到的工作内存上共享变量的值都是上一个线程修改后的最新值。

多个线程之间执行共同的代码块(访问修改共享变量),由于线程交叉执行,最终共享变量的最后值可能有多种结果:

public class SynchronizedTest {

    private boolean ready = false;
    private int result = 0;
    private int number = 1;

    public void write(){
        ready = true;
        number = 2;
    }

    public void read(){if(ready){
            result = number * 3;
        }

        System.out.println("result is " + result);
    }

    private class TestThread extends Thread{
        private boolean flag;
        public TestThread(boolean flag){
            this.flag = flag;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            if(flag){
                write();
            }else{
                read();
            }
        }
    }

    public static void main(String[] args){
        SynchronizedTest test = new SynchronizedTest();
        test.new TestThread(true).start();
        test.new TestThread(false).start();
    }
}

如上代码,由于两个线程交叉执行,最后result的结果可能是0或者6或者3

共享变量不可见主要有下列原因:

a.线程的交叉执行

b.重排序

c.共享变量未能及时更新

通过使用synchronized可以保证原子性(synchronized代码块内容要么不执行,要执行就保证全部执行完毕)和可见性,修改后的代码为在write和read方法上加synchronized关键字

4. volatile实现可见性(jdk 1.5后)

volatile如何实现可见性?

volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。

但是volatile不能保证变量更改的原子性:

比 如number++,这个操作实际上是三个操作的集合(读取number,number加1,将新的值写回number),volatile只能保证每一 步的操作对所有线程是可见的,但是假如两个线程都需要执行number++,那么这一共6个操作集合,之间是可能会交叉执行的,那么最后导致number 的结果可能会不是所期望的。

所以对于number++这种非原子性操作,推荐用synchronized:

synchronized(this){
     number++;
}

如下代码:最后的number的结果不一定是500,有可能是比500小,因为number++不是一个原子性的操作,用volatile不能保证可见性

public class VolatileTest {

    public static int number = 0;

    public void increase(){
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        number++;
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {

                @Override
                public void run() {
                    test.increase();

                }
            }).start();
        }

        //若当期依然有子线程没有执行完毕
        while(Thread.activeCount() > 1){
            Thread.yield();//使得当前线程(主线程)让出CPU时间片
        }

        System.out.println("number is " + number);
    }

}

对于自增之类的非原子性操作,只能通过如下方式保证可见性:

a. synchronized

b. ReentrantLock

c. AtomicInteger

synchronized修改如下:

public void increase(){
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        synchronized(this){
            number++;
        }

    }

ReentrantLock修改方式如下:

public class VolatileTest {

    public static int number = 0;
    public Lock lock =  new ReentrantLock();

    public void increase(){
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        lock.lock();

        try{
            number++;//这块的代码实际项目中可能会出现异常,所以要捕获
        }finally{
            lock.unlock();//用try finally块保证Unlock一定要执行
        }

    }

。。。
}

AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

修改如下:

package com.mooc.test;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest {

    public static AtomicInteger number = new AtomicInteger(0);

    public void increase(){
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        number.getAndIncrement();//获得当前值并且加1
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {

                @Override
                public void run() {
                    test.increase();

                }
            }).start();
        }

        //若当期依然有子线程没有执行完毕
        while(Thread.activeCount() > 1){
            Thread.yield();//使得当前线程(主线程)让出CPU时间片
        }

        System.out.println("number is " + number.get());
    }

}

5. volatile适用情况

a.对变量的写入操作不依赖当前值

比如自增自减、number = number + 5等(不满足)

b.当前volatile变量不依赖于别的volatile变量

比如 volatile_var > volatile_var2这个不等式(不满足)

6. synchronized和volatile比较

a. volatile不需要同步操作,所以效率更高,不会阻塞线程,但是适用情况比较窄

b. volatile读变量相当于加锁(即进入synchronized代码块),而写变量相当于解锁(退出synchronized代码块)

c. synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性

时间: 2024-10-10 06:35:56

Java多线程共享变量控制的相关文章

java多线程 -- Condition 控制线程通信

Api文档如此定义: Condition 将 Object 监视器方法(wait.notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set).其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用. Condition 接口描述了可能会与锁有关联的条件变量.这些变量在用法上与使用 Object.wait 访问的隐式监视器类似

Java多线程-线程控制

 休眠线程: public static void sleep(long millis) throws InterruptedException在指定的毫秒数内让当前正在执行的线程休眠(暂停执行).该线程不丢失任何监视器的所属权. 线程加入 public final void join() throws InterruptedException等待该线程终止. 抛出:InterruptedException - 如果另一个线程中断了当前线程.当抛出该异常时,当前线程的中断状态 被清除.  线程礼

java多线程共享变量

import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class PC { private static final Lock lock = new Reentrant

java多线程实现卖票程序

本文采用java多线程实现了模拟车站多个车票卖票的功能. 关键词:java多线程 共享变量 实现runnable接口 volatile  线程同步. 代码如下 Ticket类 package ex7_TicketSaler; /*同一对象的多个线程thread0/1/2,对共享变量count的操作,需要将count的值声明为volatile * 并且因为多个线程操作的是同一个对象ticket,因此count是资源共享的 * */ public class Ticket implements Ru

Java多线程 2 线程的生命周期和状态控制

一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常. 2.就绪状态 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它

Java多线程开发系列之四:玩转多线程(线程的控制2)

在上节的线程控制(详情点击这里)中,我们讲解了线程的等待join().守护线程.本节我们将会把剩下的线程控制内容一并讲完,主要内容有线程的睡眠.让步.优先级.挂起和恢复.停止等. 废话不多说,我们直接进入正题:  3.线程睡眠  sleep() 所有介绍多线程开发的学习案例中,基本都有用到这个方法,这个方法的意思就是睡眠(是真的,请相信我...).好吧,如果你觉得不够具体,可以认为是让当前线程暂停一下,当前线程随之进入阻塞状态,当睡眠时间结束后,当前线程重新进入就绪状态,开始新一轮的抢占计划!

Java多线程——线程的生命周期和状态控制

一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常. 2.就绪状态 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它

Java多线程(二)、线程的生命周期和状态控制(转)

Java多线程(二).线程的生命周期和状态控制 分类: javaSE综合知识点 2012-09-10 16:11 15937人阅读 评论(3) 收藏 举报 一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadSt

Java多线程之线程的控制

Java多线程之线程的控制 线程中的7 种非常重要的状态:  初始New.可运行Runnable.运行Running.阻塞Blocked.锁池lock_pool.等待队列wait_pool.结束Dead 如果将"锁池"和"等待队列"都看成是"阻塞"状态的特殊情况,那么可以将线程归纳为5个状态: 新建,就绪,运行,阻塞,死亡. ┌--------------------< 阻塞 ↓                    (1)(2)(3)