精华内容
下载资源
问答
  • Java并发编程:线程同步机制

    千次阅读 2016-01-19 18:31:17
    Java中线程同步可以通过wait、notify、notifyAll等方法实现。这几个方法在最顶级的父类Object中实现,并且被声明为final,所以子类无法重写这几个方法。在实现线程同步时,一般需要配合synchronized关键字使用,定义...

      Java中线程同步可以通过wait、notify、notifyAll等方法实现。这几个方法在最顶级的父类Object中实现,并且被声明为final,所以子类无法重写这几个方法。在实现线程同步时,一般需要配合synchronized关键字使用,定义同步代码块或者方法。JDK 1.5以后提供了Condition来实现线程间的协作,Condition提供的await、signal、signalAll方法相对于wait、notify、notifyAll的方法更加安全高效,Condition所使用的是ReentrantLock锁。

    1 synchronized关键字和ReentrantLock类

      理解synchronized关键字必须首先了解下Java的内存模型。
      Java中每一个进程都有自己的主内存,进程中的每个线程有自己的线程内存,线程从主内存中获取数据在线程内存中计算完成后回写到主内存中。在并发情况下就可能造成数据过期数据的问题。具体例子看如下代码:

    public class TestSync {
        public static int sum = 0;
        public static class MyThreadA implements Runnable {
            @Override
            public void run() {
                for (int j = 0; j < 10000; j++) {
                    sum++;
                }
            }
        }
    
        public static void main(String[] args) {
    
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) { //10个任务交给线程池, 返回的数据预期为10*10000
                MyThreadA myThreadA = new MyThreadA();
                executorService.execute(myThreadA);
            }
            executorService.shutdown();
            System.out.println(sum);
        }
    }

      执行结果如下:

    88625

      从执行结果可以看出,并不是预期中的100000。原因就在数据过期的问题。例如线程A和线程B同时从主内存中获取sum的值为1500。线程A计算了1000次,此时线程A内存中的sum为2500,并向主内存回写sum=2500,后交出CPU;线程B获得CPU开始计算了900次,此时线程B内存中的sum=2400,并向主内存回写sum=2400,后交出CPU。此时主内存的sum=2400,而预期是1500+1000+900=3400。
      使用synchronized关键字改进代码如下:

    public class TestSync {
        public static int sum = 0;
        public static Object lock = new Object(); //自定义锁对象,代价较小
        public static class MyThreadA implements Runnable {
            @Override
            public void run() {
                synchronized (lock) { //同步代码块
                    for (int j = 0; j < 10000; j++) {
                        sum++;
                    }
                }
            }
        }
    
        public static void main(String[] args) {
    
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) { //10个任务交给线程池, 返回的数据预期为10*10000
                MyThreadA myThreadA = new MyThreadA();
                executorService.execute(myThreadA);
            }
            executorService.shutdown();
            System.out.println(sum);
        }
    }

      执行结果如下:

    100000

      执行结果符合预期。原因是线程进入同步代码块后会获取对象锁,阻止其他线程进入执行,线程执行完for循环并向主内存回写sum后才会退出退出同步代码块,其他线程才会执行。
      ReentrantLock类提供的锁机制可以完成所有synchronized关键字能实现的功能并且针对synchronized的限制 — 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁,做出了改进,提高了高争用条件下的执行效率。具体分析请参考(https://www.ibm.com/developerworks/cn/java/j-jtp10264/);

    2 wait()、notify()、notifyAll() 介绍及代码演示(介绍纯属copy其他博客, 地址:http://blog.csdn.net/ns_code/article/details/17225469

    2.1 wait方法介绍

      该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。

    2.2 notify方法介绍

      该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。

    2.3 notifyAll方法介绍

      该方法与notify()方法的工作方式相同,重要的一点差异是:notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

    2.4 更深入的理解

      如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
      优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

    2.5 代码展示

      3线程交替wait,相互唤醒。

    public class Main {
        private static Object lock = new Object(); //自定义锁对象
        public static class MyThreadA implements Runnable {
            public String name;
            public MyThreadA(String name) {
                this.name = name;
            }
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 11; i++) {
                        if (i % 3 == 0 && i != 0) {
                            System.out.println(Thread.currentThread().getName() + " : "+ i);
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                            }
                        }
                        //System.out.println(Thread.currentThread().getName() + " : " + i + "notify");
                        lock.notify();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 3; i++) {
                MyThreadA myThreadA = new MyThreadA("thread a");
                executorService.execute(myThreadA);
            }
            //executorService.shutdown();
        }

      执行结果如下:

    pool-1-thread-1 : 3
    pool-1-thread-2 : 3
    pool-1-thread-3 : 3
    pool-1-thread-1 : 6
    pool-1-thread-3 : 6
    pool-1-thread-1 : 9
    pool-1-thread-3 : 9
    pool-1-thread-2 : 6

      线程2打印6后进入wait,没有其他线程notify导致一直等待。

    3 await()、signal()、signalAll()介绍及代码演示

      这三个方法的作用和wait、notify、notifyAll类似,采用这三个方法是需要使用的同步锁是Lock。

    public class Main {
        private static Lock lock = new ReentrantLock(); //自定义锁对象
        private static Condition condition = lock.newCondition();
        public static class MyThreadA implements Runnable {
            public String name;
            public MyThreadA(String name) {
                this.name = name;
            }
            @Override
            public void run() {
                lock.lock();
                try {
                    for (int i = 0; i < 11; i++) {
                        if (i % 3 == 0 && i != 0) {
                            System.out.println(Thread.currentThread().getName() + " : " + i);
                            try {
                                condition.await();
                            } catch (InterruptedException e) {}
                        }
                        //System.out.println(Thread.currentThread().getName() + " : " + i + "notify");
                        condition.signalAll();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 3; i++) {
                MyThreadA myThreadA = new MyThreadA("thread a");
                executorService.execute(myThreadA);
            }
            executorService.shutdown();
        }
    }
    

    执行结果如下:

    pool-1-thread-1 : 3
    pool-1-thread-2 : 3
    pool-1-thread-1 : 6
    pool-1-thread-2 : 6
    pool-1-thread-1 : 9
    pool-1-thread-2 : 9
    pool-1-thread-3 : 3
    pool-1-thread-3 : 6

      和使用wait、notify类似,最后一个线程有可能会陷入一直等待的状态。

    4、下一篇分别用上面描述的两种方法实现经典的生产者-消费者模型

    展开全文
  • JAVA多线程——线程同步机制,同步方法和同步块 并发:同一个对象被多个线程同时操作 线程同步:处理多线程问题时,多个线程访问同一个对象,并且某个对象还想修改这个线程。这时候就需要线程同步。线程同步其实就是...

    JAVA多线程——线程同步机制,同步方法和同步块

    并发:同一个对象被多个线程同时操作

    线程同步:处理多线程问题时,多个线程访问同一个对象,并且某个对象还想修改这个线程。这时候就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

    锁机制:由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来的访问冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,就能独占并使用对象资源,其他线程必须等待,使用后释放锁即可。

    锁机制存在的问题:

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

    锁机制包含两种用法:synchronized同步方法和synchronized同步块

    同步方法

    synchronized同步方法控制对对象的访问,每个对象对应一把锁,每个synchronized同步方法都必须获得调用该方法的锁的对象才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

    缺陷:若将一个大的方法申明为synchronized同步方法将会影响效率

    方法里面需要修改的内容才需要锁,锁的太多,浪费资源

    买火车票的案例

    package com.peng.cny;
    //买火车票的案例
    public class UnsafeBbuyticket {
        public static void main(String[] args) throws InterruptedException {
            BuyTicket buyTicket = new BuyTicket();
    
            new Thread(buyTicket,"车站").start();
            new Thread(buyTicket,"高铁").start();
            new Thread(buyTicket,"飞机").start();
    
            Thread.sleep(100);
        }
    }
    class BuyTicket implements Runnable{
    
        private int ticketNums=10;
        boolean flag = true;//线程的外部停止方式
        @Override
        public void run() {
            while (flag==true){
                buy();//调用买票的方法
            }
        }
    private  void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
        }
    }
    

    会拿到同一张票,不安全
    在这里插入图片描述

    用了synchronized同步方法后

    package com.peng.cny;
    //买火车票的案例
    public class UnsafeBbuyticket {
        public static void main(String[] args) throws InterruptedException {
            BuyTicket buyTicket = new BuyTicket();
    
            new Thread(buyTicket,"车站").start();
            new Thread(buyTicket,"高铁").start();
            new Thread(buyTicket,"飞机").start();
    
            Thread.sleep(100);
        }
    }
    class BuyTicket implements Runnable{
    
        private int ticketNums=10;
        boolean flag = true;//线程的外部停止方式
        @Override
        public void run() {
            while (flag==true){
                buy();//调用买票的方法
            }
        }
        //synchronized同步方法,锁的是this,是本身的类的对象
    private synchronized void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
        }
    }
    

    拿票就是有顺序了,也不会重复拿

    在这里插入图片描述
    用synchronized同步方法解决继承Thread类的线程不安全问题

    package com.work;
    //用synchronized同步方法解决继承Thread类的线程不安全问题
    public class work5 {
        public static void main(String[] args) {
            Window4 window4 = new Window4();
            Window4 window1 = new Window4();
            Window4 window2 = new Window4();
    
            window1.start();
            window2.start();
            window4.start();
        }
    }
    class Window4 extends Thread{
       private static int tickets = 100;
    
        @Override
        public void run() {
    
            while (true) {
                buy();
            }
        }
        //在这里不能直接用synchronized同步方法,因为这里锁的对象由三个window4 window1 window2,不是this
        //变成静态方法后,这时候的锁(同步监视器)就是唯一的,是Window4.class这个类   (在静态方法里面不能调this)
        private static synchronized  void buy(){
            if (tickets>0){
                System.out.println(Thread.currentThread().getName()+" "+tickets);
            tickets--;
            }
        }
    }
    

    synchronized同步方法的总结:

    • 同步方法法仍然涉及到同步监视器(锁),只是不需要显式的声明
    • 静态方法的同步监视器是当前类本身
    • 非静态方法的同步监视器是this

    同步块

    同步块:synchronized(Obj){
    需要被同步的代码块(操作共享数据的代码即为需要被同步的代码)
    }

    Obj称为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 锁的对象是变化的量,是需要增删改查的量
    • 同步方法中不需要指定同步监视器,因为同步方法中的同步监视器就是this,就是这个对象本身,或者是class

    两个人同时去银行取同一个账户的钱案例(实现Runnable接口)

    package com.peng.cny;
    //两个人同时去银行取钱
    public class UnsafeBank {
        public static void main(String[] args) {
    
            Account account = new Account(100,"马化疼的账户");
            Drawing you = new Drawing(account,50,"yon");
            Drawing tx= new Drawing(account,100,"tx");
            
            you.start();
            tx.start();
        }
    }
    
    //银行账户
    class Account{
        int money;
        String name;
        //构造器,初始化变量
        public Account(int money,String name) {
            this.money = money;//余额
            this.name = name;//卡名
        }
    }
    //银行取款
    //用继承Thread的方式创建线程,因为不涉及到多个线程操作同个对象
    class Drawing extends Thread{
        Account account;//取钱用的账户account
        int drawingMoney;//取了多少钱
        int nowMoney;
        public Drawing( Account account,int drawingMoney,String name){
            super(name);
            this.account=account;
            this.drawingMoney=drawingMoney;
        }
        @Override
        public  void run() {
           //锁的对象是变化的量,需要增删改查的量
    
                //判断卡里的钱够不够
                if (account.money<drawingMoney){
                    System.out.println(Thread.currentThread().getName()+"取得时候钱不够,取不了");
                    return;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                //卡剩下的钱
                account.money = account.money - drawingMoney;
                //你手里的钱
                nowMoney = nowMoney + drawingMoney;
                System.out.println(account.name+"余额为"+account.money);
    
                // this.getName()= Thread.currentThread().getName()
                System.out.println(this.getName()+"手里的钱"+nowMoney);
    
            }
        }
    

    两个人都成功的取出了想要的钱,导致账户余额变为负数
    在这里插入图片描述

    用了synchronized同步块后

    package com.peng.cny;
    //两个人同时去银行取钱
    public class UnsafeBank {
        public static void main(String[] args) {
    
            Account account = new Account(100,"马化疼的账户");
            Drawing you = new Drawing(account,50,"yon");
            Drawing tx= new Drawing(account,100,"tx");
    
    
            you.start();
            tx.start();
        }
    }
    
    //银行账户
    class Account{
        int money;
        String name;
        //构造器,初始化变量
        public Account(int money,String name) {
            this.money = money;//余额
            this.name = name;//卡名
        }
    }
    //银行取款
    //用继承Thread的方式创建线程,因为不涉及到多个线程操作同个对象
    class Drawing extends Thread{
        Account account;//取钱用的账户account
        int drawingMoney;//取了多少钱
        int nowMoney;
        public Drawing( Account account,int drawingMoney,String name){
            super(name);
            this.account=account;
            this.drawingMoney=drawingMoney;
        }
        @Override
        public  void run() {
           //锁的对象是变化的量,需要增删改查的量
            synchronized(account){// synchronized同步块可以锁任何对象
                //判断卡里的钱够不够
                if (account.money<drawingMoney){
                    System.out.println(Thread.currentThread().getName()+"取得时候钱不够,取不了");
                    return;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                //卡剩下的钱
                account.money = account.money - drawingMoney;
                //你手里的钱
                nowMoney = nowMoney + drawingMoney;
                System.out.println(account.name+"余额为"+account.money);
    
                // this.getName()= Thread.currentThread().getName()
                System.out.println(this.getName()+"手里的钱"+nowMoney);
    
            }
        }
    }
    

    不能随便的取钱了,要看余额有多少

    在这里插入图片描述
    用同步代码块处理继承Thread类的安全问题

    package com.work;
    //用同步代码块处理继承Thread类的安全问题
    public class work4 {
        public static void main(String[] args) {
            
            BuyTicket2 ticket2 = new BuyTicket2();
            BuyTicket2 ticket3 = new BuyTicket2();
            BuyTicket2 ticket4 = new BuyTicket2();
    
            ticket2.setName("线程一");
            ticket3.setName("线程二");
            ticket4.setName("线程三");
    
            ticket2.start();
            ticket3.start();
            ticket4.start();
        }
    }
    class BuyTicket2 extends Thread{
        private static int tickets = 100;
    //因为上面创建了三个对象,所以要让三个线程共享着一个锁obj,就要加static
      private  static Object  obj = new Object();
        @Override
        public void run() {
            while (true) {
                synchronized(obj) {
                    //也可以拿BuyTicket2.class)充当锁
                    //synchronized(BuyTicket2.class){
                    if (tickets > 0) {
    
                        System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票");
                        tickets--;
                    } else {
                        break;
                    }
                    // }
                }
    
            }
        }
    }
    

    线程本身不安全案例

    package com.peng.cny;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class UnsafeList {
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();//建一个数组
    
            for (int i = 0; i <20000 ; i++) {
                new Thread(()->{
                    //把20000条线程添加进去      
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());//打印数组大小
        }
    }
    

    并没有添加20000条线程

    在这里插入图片描述

    用了同步块锁住对象后

    package com.peng.cny;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class UnsafeList {
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
    
            for (int i = 0; i <20000 ; i++) {
                new Thread(()->{
                    synchronized (list) {
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
    
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());//打印数组大小
        }
    }
    

    在这里插入图片描述

    展开全文
  • 线程同步机制

    千次阅读 2019-02-14 17:52:46
    从广义上说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字和一些相关的API,如Object.wait( )/.notify( )等   1、锁的概述和概念: a 线程安全问题的产生: 多个线程并发访问...

     

     

    一、线程同步机制

    从广义上说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字和一些相关的API,如Object.wait( )/.notify( )等

     

    1、锁的概述和概念:

    a 线程安全问题的产生:

    多个线程并发访问共享变量、共享资源;

    解决办法:

    一个共享变量或者资源只能被一个线程访问,访问结束后其他线程才能访问(用锁)。

     

    b、用锁保护状态:

    1、对于可能被多个线程同时访问的可变状态变量(比如银行总账户),在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是有这个锁保护的。

    2、每个共享的和可变的变量都应该只有一个锁来保护,从而使维护人员知道是哪一个锁。

    3、对于每个包含多个变量的不变性条件,期中涉及的所以变量都需要由同一个锁来保护。

     

    • 许多线程安全类使用的加锁模式是,将可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码进行同步
    • 每个对象都有一个内置锁,只是为了免去显式地创建锁对象

     

    c、锁的几个概念

    锁的争用:锁可以被看做多线程程序访问共享数据时所持有的一种排他性资源

    锁的调度:包括公平调度策略和非公平调度策略。内部锁属于非公平锁;显示锁则两者都支持。

    锁的粒度:一个锁保护的共享数据的大小称粒度。锁的粒度过粗会导致线程在申请锁的时候需要进行不必要的等待,影响性能。

    锁的开销:锁的开销主要包括锁的申请和释放产生的开销,锁可能导致上下文切换,开销主要是处理器时间。

    锁可能导致的问题:

    a.锁泄漏:指一个线程获得锁之后,由于程序的错误,致使该锁一直无法被释放而导致其他线程一直无法获得该锁的现象。

    b.锁的不正当使用还会导致死锁、锁死等线程活性故障

     

    d、锁的作用:

    保护共享数据以实现线程安全,包括保障原子性、可见性和有序性(原子性和可见性>>有序性)

    原子性:锁通过互斥来保障原子性,互斥是指一个锁一次只能被一个线程所持有,所以,临界区代码只能被一个线程执行,即保障了原子性。

    可见性:通过写线程冲刷处理器缓存和读线程刷新处理器缓存实现。获得锁之后,需要刷新处理器缓存,使得前面写线程所做的更新可以同步到本线程。释放锁需要冲刷处理器缓存,使得当前线程对共享数据的改变可以被推送到下一个线程处理器的高速缓冲中。

    有序性:写线程在临界区中所执行的一系列操作在读线程所执行的临界区看起来像是完全按照源代码顺序执行的。但是并不能保证不重排,只是重排不会影响。

     

    锁在保证线程安全的同时满足三大条件,那么我们必须遵守:

    1.线程访问同一组数据的时候必须使用同一个锁

    2.线程中任意的一个线程,即使其仅仅是你读取这组共享数据而没有对其进行更新的话,也需要在读取时持有相应的锁。

     

    锁其实就是把本来并发(未使用锁)的线程改成串行(使用锁)。

     

    e、锁的适用场景

          • check-then-act操作:一个线程读取共享数据并在此基础上决定下个操作是什么。
          • read-modify-write操作:一个线程读取共享数据并在此基础上更新该数据。
          • 多个线程对多个共享数据进行更新:共享数据之间存在关联关系

    2、内存屏障和重入:

    a、线程同步机制的底层助手:内存屏障

    内存屏障:在指令序列中就像一堵墙一样使其两侧的指令无法穿越(即不可以重排序)

    内存屏障在锁中的使用:

          1. 获取锁
          2. 加载屏障
          3. 获取屏障
          4. 临界区
          5. 释放屏障
          6. 存储屏障

    (其中:3和5用来禁止指令重排序)

    Java线程同步机制就是使用内存屏障在具体实现的

     

    b 重入:

    可重入是指对于同一个线程,它可以重新获得已有它占用的锁。 

          • 如果没有重入,当某个线程请求一个由其他线程持有的锁时,发送的请求的线程就会阻塞。
          • 由于内置锁可以重入,因此如果某个线程试图获得一个已经由它持有的锁,那么请求就会成功。

    可重入性:一个线程在持有一个锁的时候可以再次申请该锁

    如何实现可重入性?可重入锁可以被理解为一个对象,该对象包含一个计数器属性,获取锁计数器+1,释放锁计数器-1;

     

    //(比如下面方法,一个同步方法调用另一个同步方法,没有重入的话,等待当前同步结束才能调用新的同步则发生阻塞,重入则避免了这种情况) public class Test { public synchronized f() {} public synchronized g() { // 可以重入,所以当前已经获得锁的线程可以获得f的锁,否则死锁 f(); } }

     

    3、Java虚拟机对锁的实现划分:

    内部锁:通过synchronized关键字实现 独占锁,在高并发访问情况下,可能会引起上下文切换和线程调度

    显示锁:通过java.util.concurrent.locks.Lock接口的实现类实现

     

     

    二、发布和溢出

    我们知道线程安全问题的产生前提是多少线程共享变量,即使是private变量也可能多被个线程共享。

    public class Example{ private Map<String,Integer> reg = new HashMap<String,Integer>(); public void do(){ //reg操作 } }

    如上述代码,多线程多do进行操作时,priavte变量相当于被多个线程共享,就这是发布。当然可以用volatile来保证安全。

     

    1、发布:

      是对象能够在当前作用域之外的代码中使用。可以是以下几种情况:

    1. 一,将对象的引用保存在公有变量或公有静态变量中

    public class Test { public static List<People> list; public Test(){ list = new ArrayList<People>(); } }

    通过list对象可以轻易的遍历list对象中保存的People对象,实际上list对象中保存的People对象也被间接的发布了

     

    二,在一个非私有的方法中返回该引用

    public class Test { private String[] strs = {"AB","BA"}; public String[] getStrs() { return strs; } }

    通过getStrs()方法可以获得本来应该被封装在Test类内部的strs数组

     

    三,将对象引用传递给外部方法

    外部方法:对当前类来说,外部方法是指行为不完全由当前类来规定的方法,包括其他类中定义的方法以及当前类中可以被改写的方法(既不是私有方法,也不是final方法)

    当把一个对象传递给外部方法时,就相当于发布了这个对象

    public class Test { public void get(Object obj){ //obj对象逸出 ... } }

     

    2、逸出:

    某个不应该发布的对象被公布的时候。某个对象逸出后会导致对象的内部状态被暴露,可能危及到封装性,使程序难以维持稳定;若发布尚未构造完成的对象,可能危及线程安全问题。 你必须假设有某个类或线程可能会误用该对象,所以要封装。

    • 不要在构造过程中使this引用逸出。
    • 常见错误:在构造函数中启动一个线程。

     

    最常见的逸出是this引用在构造时逸出,导致this引用逸出的常见错误有:

    1、在构造函数中启动线程:

    当对象在构造函数中显式还是隐式创建线程时,this引用几乎总是被新线程共享,于是新的线程在所属对象完成构造之前就能看见它。

    避免构造函数中启动线程引起的this引用逸出的方法是不要在构造函数中启动新线程,取而代之的是在其他初始化或启动方法中启动对象拥有的线程。

     

    2、在构造方法中调用可覆盖的实例方法:

    在构造方法中调用那些既不是private也不是final的可被子类覆盖的实例方法时,同样导致this引用逸出。

    避免此类错误的方法时千万不要在父类构造方法中调用被子类覆盖的方法。

     

    3、在构造方法中创建内部类:

    在构造方法中创建内部类实例时,内部类的实例包含了对封装实例的隐含引用(深入理解 内部类),可能导致隐式this逸出。例子如下:

    public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }

    上述例子中的this逸出可以使用工厂方法来避免,例子如下:

    public class SafeListener { private final EventListener listener; private SafeListener(){ listener = new EventListener(){ public void onEvent(Event e){ doSomething(e); } ); } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }

     

     

     

    3、对象的初始化安全:重访final和static:

    安全的发布

    (1)      使用synchronized

    public classSafeLazyInitialization { private static Resource resource; public synchronized static ResourcegetInstance() { if(resource == null) { resource = new Resource(); } return resource; } }

    (2)      使用volatile

    class Foo { private static volatile Helper helper = null;//使用volatile修饰使resource的读操作与写操作具有happen-before规则 public static Helper getHelper() { if (helper == null) synchronized(Foo.class) { if (helper == null) helper = new Helper(); } return helper; } // other functions and members... }

     

    (3)      静态初始化器(利用JVM锁机制提前初始化)

    在初始器中采用了特殊的方式来处理静态域,并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行即在类被加载后并且在被线程使用之前,由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则只适用于构造时的状态,也就是说保证构造的完整性,并不保证之后的操作的线程安全。如果这个类不是线程安全的,仍需要适当的同步。

    public classEagerInitialization{ private static Resource resource = new Resource(); public synchronized static ResourcegetResource() { return resource; } }

     

    (4)      延迟初始化占位

    public classResourceFactory{ private static class ResourceHolder{ private static Resource resource = new Resource(); } public synchronized static ResourcegetResource() { return ResourceHolder.resource; } }

     

    (5) Final

    public class SafeState{ private final Map<String,String> states; public SafeState(){ states = new HashMap<String,String>();//final域写入操作 state.put("a","a");//final域可达的变量,仍然具备可见性,并且不会被重排序到构造函数之后 } }

     

    根据上面所讨论的,可以总结出以下安全发布的常用模式:

    注意, 我们这里的目的是:在对象没有完成构造之前,不可以将其发布,不安全,不推荐(要安全 = 不能随便new一个对象出来)

    不正确的发布可变对象,导致两种错误。

    • 发布线程以外的任何线程都可以看到被发布对象的过期的值,
    • 线程看到的被发布对象的引用是最新的,然而被发布对象的状态是过期的,如果一个对象是可变对象,那么它要被安全发布才可以。

     

     

     

     

    三、线程封闭

      当访问共享的可变数据时,通常需要使用同步。一种避免同步的方式就是不共享数据。

      仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement)。

      典型应用:

    ①Swing的可视化组件和数据模型对象都不是线程安全的,Swing通过将它们封闭到Swing的实际分发线程中来实现线程安全;

    ②JDBC的Connection对象。

     

    线程封闭技术:

    ①Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。

      在volatile变量上存在一个特殊的线程封闭:能确保只有单个线程对共享的volatile变量执行写入操作(其他线程有读取volatile变量),那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作,而其他读取volatile变量的线程也能看到最新的值。

     

    ②栈封闭:在栈封闭中,只能通过局部变量才能访问对象。

    public int loadTheArk(Collection<Animal> candidates){ SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; //animals被限制在本地方法栈中 animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for(Animal a : animals){ if(candidate == null || !candidate.isPotentialMate(a)){ candidate = a; }else{ ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; } 指向TreeSet对象的唯一引用保存在animals中,而animals这个引用被封闭在局部变量中,因此封闭在线程本身的工作内存中,其它线程不能访问。 如果发布了对集合的引用,那么线程的封闭性将被破坏,并且导致对象animals的逸出。

      

    ③ThreadLocal类:这是线程封闭的更规范方法,这个类能使线程中的某个值与保存值的对象关联起来。

      提供get()和set()方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

      ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。怎么理解呢?还是JDBC的Connection对象,防止共享,所以通常将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。

     

    展开全文
  • windows系统多线程同步机制原理总结

    千次阅读 2018-12-24 21:24:33
    windows系统多线程同步机制原理总结 同步问题是开发过程中遇到的重要问题之一。同步是要保证在并发执行的环境中各个控制流可以有序地执行,包括对于资源的共享或互斥访问,以及代码功能的逻辑顺序。 为了保证多线程...

    windows系统多线程同步机制原理总结

    同步问题是开发过程中遇到的重要问题之一。同步是要保证在并发执行的环境中各个控制流可以有序地执行,包括对于资源的共享或互斥访问,以及代码功能的逻辑顺序。
    为了保证多线程间的同步,Windows操作系统提供了一系列的机制:事件、互斥体、信号量等等。本文主要基于对《Windows内核原理与实现》一书相关章节的整理并结合自己的理解介绍同步机制的大概实现原理,有任何不妥的地方,希望大家能够不吝指出。

    Windows线程调度

    我们先来简单的说以下Windows的线程调度,这是理解后续东西的基础。Windows的调度算法是一个抢占式的、支持多处理器的优先级调度算法。这种调度算法将处理器的时间分成一个个的时间片段,每个时间片段分给不同的线程去执行线程中的指令,直到时间片用完。显然,这样做使得处理器的执行不是按照一个事先知道的顺序依次进行;操作系统在调度线程时需要一个时钟中断来获得对处理器的控制权,从而引导处理器去执行操作系统想要它执行的目标线程的指令。

    中断

    说到Windows的线程调度是通过中断来切换线程的,那么到底什么是中断呢?我们这样想:处理器在执行指令时肯定是顺序执行指令流,那么它怎么处理一些事先无法预知、运行时才发生的事件呢?比如用户某一时刻突然敲下键盘。一种办法是处理器足够频繁地去挨个检查所有可能发生的事件是否发生了,这种方法显然很笨;另一种方法就是使用中断。
    中断其实包含硬件中断和软件中断,说说硬件中断(软件中断其实是模拟的硬件中断)。硬件中断是外部设备在需要通知处理器去做一件事的时候向处理器的特定管脚(NMI和INTR)发送数据,处理器每执行完一条指令都会去检查这个管脚的状态,看看是否有中断发生。通过中断机制,处理器就不需要去挨个设备的检查了,只需要统一的检查是否发生了中断。那么,处理器在得知发生中断后,又怎么知道发生了什么中断?以及怎么去处理中断呢?
    原来,每个中断都有一个中断编号,也称为中断向量。外部设备在触发处理器中断时会发生相应的中断向量。除此之外,操作系统中维护一个中断描述符表(IDT),这个表将每个中断向量与中断服务例程(用来处理该中断的一段程序)关联起来。处理器根据中断向量查询IDT,从而找到中断服务例程的地址,去执行中断服务例程,完成对中断的响应。

    同步机制的实现

    现在我们明白了操作系统是怎么调度线程的了,这是这种调度方式使得线程的同步有了很多不同的方式。那怎么实现呢?

    1. 不依赖于线程调度的同步机制

    第一种做法是对中断下手。需要申明,这种方法只对单处理器有效。单处理器情况下,导致资源争用的罪魁祸首是线程调度,它使得一个处理器分时并行地干几件事,就好像有多个处理器一样。那不让线程调度不就行了?对!前面说到了,线程调度是依靠中断实现的,那就从中断下手。处理器在处理中断时,并不是什么中断都处理,它会判断当前条件下是否需要处理某一中断。而Windows提供了一套中断级别定义方案,叫做中断请求级别(IRQL)。它使用0~31来表示优先级,数值越大,优先级越高;处理器任何时候都运行在一个级别上,且只能被更高级别的中断打断。感兴趣的同学可以去查查IRQL到底包含哪些级别,本文只说其中一个级别——DISPATCH_LEVEL,从名称中可以大致猜到这是线程调度时所在的级别。因此,如果在访问需要同步的资源之前,将处理器的运行级别提高到DISPATCH_LEVEL或更高的IRQL,这时操作系统就不会调度线程了,对单处理器而言就不存在其他线程同时访问资源的情况,也就实现了目标。

    下面划重点:自旋锁我们都知道。它的实现就利用了这种方式,因此,线程在自旋等待时不会有线程切换。也正是因为不会有线程切换,省去了切换的耗时,因此它很适合预期等待时间很短的情况使用。

    2. 基于线程调度的同步机制

    第二种方法我们不去干涉线程调度。那么要想让不同线程能够协调起来,我们需要一个全局的东西去协调它们。这个东西就是同步对象,也称为分发器对象。无论是互斥体还是信号量还是其他什么同步方式,它们都需要针对一个确定的分发器对象。一个分发器对象有基本的两个状态:有信号和无信号状态,分发器对象初始化时为有信号状态。当一个线程执行某种同步方式时,它去查看指定分发器对象是否有信号,若有信号,该线程继续执行,同时该分发器对象的状态变为无信号;再有别的线程去查看该分发器对象时,发现状态为无信号,此时该线程进入等待状态(睡眠),操作系统的线程调度将不再调度该线程。这就实现了只能一个线程访问同一资源。

    那么,当第一个线程执行完了后,分发器对象的状态重新设定为有信号后,其他在等待该分发器对象的线程又怎么知道呢?这就涉及到线程对象和分发器对象的数据结构。在线程对象和分发器对象中都有同样的一个数据成员:一个指向某一链表头节点的指针,而这个链表是一个等待块对象链表。每个等待块对象都记录了哪个线程在等待哪个对象。是的,等待块对象会同时加入到两个链表中:一个是线程对象中的链表,一个是分发器对象中的链表。
    现在我们回到之前的情形,当别的线程去查看该分发器对象时,发现状态为无信号,此时实例化一个等待块对象,它记录了当前线程在等当前分发器对象,这个等待块对象都添加到了线程对象和分发器对象的链表中;当分发器对象重新变为有信号后,它会去唤醒等待块链表中记录的线程对象,此时等待的线程的状态变成延迟的就绪状态,等待操作系统线程调度器来调度。

    分发器对象有很多种,具体可分为:事件、突变体、信号量、进程、线程、队列、门、定时器。有些名字听起来是不是很熟悉?是的,它们都对应着不同的同步方式:比如事件就对应同步中的事件、突变体对应互斥体…这些分发器对象在数据结构上分为两部分:对象头部和对象体。不同分发器的对象头部是一样的(对象头部的第三个数据正是等待块链表的头指针),对象体因功能不同而不同(类似于面向对象里的多态性)。对象体不同,正是与之对应的同步方式功能有所不同的本质原因。比如说突变体对象的对象体中有一个所有者概念,用于表示当前拥有该突变体对象的线程(我认为这就是互斥锁能作为递归锁的原因)。

    需要说一下,这些同步方式都需要处理器提供支持。如果处理器不提供原子运算的功能,那么什么事件、信号量等等都是空谈,因为至少在对分发器对象进行判断的时候必须要保证其过程是原子操作。现代处理器会提供一些原子运算指令,比如在Inter x86指令体系中,有些运算指令加上lock前缀就可以保证其原子性,lock前缀指令使用的两个条件:

    1. 指令的目标操作数必须一个是内存操作数;
    2. 仅适用于ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD、XCHG。

    最后总结一下,我们常见的线程同步方式,如事件、互斥体等,它们的实现都是依靠于分发器对象。操作系统通过查看分发器对象的状态,判断一个线程是等待还是执行。

    展开全文
  • 小实验三:根据同步机制的Peterson软件解决方案尝试自己编程实现线程同步机制和用于上述线程并发问题的解决,并基于程序运行时间长短将其与基于Windows互斥信号量的线程同步机制的效率展开比较。 实验要求:线程主体...
  • 前言:相信大家在进行Java开发的时候经常会接触到同步的概念,在多线程并发的情况下,为保证同一个时间点只能被一个线程访问到,就需要用到同步机制。想要了解更多关于Java多线程知识,请移步:Android多线程机制...
  • innodb是一个多线程并发的存储引擎,内部的读写都是用多线程来实现的,所以innodb内部实现了一个比较高效的并发同步机制。innodb并没有直接使用系统提供的锁(latch)同步结构,而是对其进行自己的封装和实现优化,...
  • 在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。 对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal...
  • Java 并发变成同步机制

    千次阅读 2016-02-28 16:56:01
    在多线程变成中,由于多个线程共享进程的变量,有可能出现同时访问一个资源的情况,因此需要使用同步机制。 java的内存模型: Java内存模型规定所有的变量都存在主存当中,每个线程都有自己的工作内存,线程...
  • threadlocal和线程同步机制的比较

    千次阅读 2012-09-13 16:37:12
    threadLocal和其他所有的同步机制都是为了解决多线程中的对同一变量的访问冲突。  在普通的同步机制中,是通过对象加锁来实现多个线程对统一变量的安全访问的,这时该变量是多个线程共享的,使用这种同步机制需要...
  • 概述线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。 因此为了解决这个问题,我们...线程同步synchro
  • 解析JVM线程同步机制

    万次阅读 2005-12-30 17:55:00
    解析JVM线程同步机制 田海立 2005年12月26日 摘要对多线程的支持一般是在OS级的,而Java将其做在了语言级别,这其中最吸引人的莫过于Java对线程同步(互斥与协作)的支持。本文分析了JVM(Java Virtual Machine)...
  • 线程同步互斥机制

    千次阅读 2018-04-23 17:20:35
    线程同步互斥机制 线程互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 例如 : 出现这种情况是因为操作系统在内核中对...
  • C++多线程并发(三)---线程同步之条件变量

    千次阅读 多人点赞 2019-05-03 12:43:12
    在前一篇文章《C++多线程并发编程(二)—线程同步之互斥锁》中解释了线程同步的原理和实现,使用互斥锁解决数据竞争访问问题,算是线程同步的加锁原语,用于排他性的访问共享数据。我们在使用mutex时,一般都会期望...
  • java多线程并发机制

    千次阅读 2017-04-27 10:59:33
    一、多线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • C++多线程并发(二)---线程同步之互斥锁

    万次阅读 多人点赞 2019-03-20 00:08:29
    一、何为线程同步 在前一篇文章《C++多线程并发编程(一)—线程管理》中解释多线程并发时说到两个比较重要的概念: 多线程并发:在同一时间段内交替处理多个操作,线程切换时间片是很短的(一般为毫秒级),一个...
  • 首先,我们要清楚的是wait/notify操作是一种线程同步机制。 为什么要同步?因为多个线程并发的访问同一共享变量,所以需要线程间同步,防止他们之间相互冲突。 同步的关键字是synchronized,并没有明确的锁的...
  • android多线程并发协调semaphore机制

    千次阅读 2016-04-20 17:45:23
    在项目开发过程中我们难免用到多线程机制,但不可否认的是如果对公共资源共同访问时候没有处理好线程同步的话很容易出现非线程安全的问题,会带来不可预知的错误,在java中进行线程同步的话一般都用wait和notify机制,...
  • 上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile。volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新...
  •   1、确保线程互斥访问同步代码;   2、保证共享变量的修改能够及时可见;   3、有效解决指令重排序问题。   synrhronized关键字简洁、清晰、语义明确,因此即使有了Lock接口,使用的还是非常广泛。其应用层...
  • C#实现多线程同步并发操作,在线源码,供你下载学习
  • 关于事务并发线程同步概念问题

    千次阅读 2016-11-06 23:33:30
    事务,是并发控制的单位,解决了多个客户端并发方式访问数据库 ,造成的 并发问题。 多线程的好处:开启多个...多线程安全问题的解决办法: 同步。 ============================================ 问题1: 多

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 228,227
精华内容 91,290
关键字:

并发线程的同步机制