java从基础知识(十)java多线程(下)

  首先介绍可见性、原子性、有序性、重排序这几个概念 

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

可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到

  共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

  每个线程都有自己的工作内存,存有主内存中共享变量的副本,当工作内存中的共享变量改变,会主动刷新到主内存中,其它工作内存要使用共享变量时先从主内存中刷新共享变量到工作内存,这样就保证了共享变量的可见性。

  

可见性的实现方法:

  1、synchronized两条规则

    线程解锁前,必须要把共享变量的最新值刷新到主内存中

    线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要是同一把锁)

    总之,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

  2、volatile

    只能保证内存的可见性,不能保证操作的原子性。

有序性:即程序执行的顺序按照代码的先后顺序执行。

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

    1、编译器优化的重排序(编译器优化)

    2、指令集并行重排序(处理器优化)

    3、内存系统重排序(处理器优化)

    as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)

    重排序不会给单线程带来内存可见性问题

    多线程中程序交错执行,重排序可能会造成内存可见性问题

线程不可见的原因:

    1、线程的交叉执行(需要原子性)

    2、重排序结合线程交叉执行(需要原子性)

    3、共享变量更新后的值没有在工作内存与贮存间及时更新(需要内存可见性)

1、线程同步

  线程同步是保证多个线程安全访问竞争资源的一种手段。

  1.1、通过synchronized关键字(修饰方法、代码块)

  synchronized保证锁内操作的原子性,内存的可见性。

  县城执行互斥代码的过程:获取互斥锁、清空工作内存、从住内存中拷贝最新变量到工作内存、执行代码、将更改后的共享变量的值刷新到住内存、释放互斥锁。

public class SynchronizedTest {
    public static void main(String[] args) {
        final Outputter output = new Outputter();
        new Thread() {
            public void run() {
                output.output("Thread ");
            }
        }.start();
        new Thread() {
            public void run() {
                output.output("synchronized ");
            }
        }.start();
    }
}

class Outputter {
    public synchronized void output(String name) {
        /*synchronized (this) {*/
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /*}*/
    }
}

  上述代码可以保证两个单词不被拆分,但不能保证其顺序,通过join方法可实现顺序输出。如果去掉synchronized两个单词将被拆分输出。

  注意:当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)代码块

       当一个线程访问object的一个synchronized(this)同步代码块时,其它线程对object中所有其它synchronized(this)代码块的访问将会被阻塞。

     当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  1.2、通过域变量(volatile)实现线程同步(变量)

  volatile能够保证内存的可见性,不能保证操作的原子性。

  volatile确保变量每次使用的时候是从主存中获取,而不是每个线程各自的工作内存,volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”(因为线程对共享资源的读写不具有原子性),也就是说不保证线程执行的有序性。

  volatile实现内存的可见性是通过内存屏障和禁止重排序优化来实现的,对volatile变量执行写操作时,会在写操作后加入一条store屏障指令(刷新变量到主内存),对volatile变量执行读操作时会在都操作前加入一条load屏障指令(重主内存中读取变量)。

public class VolatileDemo {

    private volatile int number = 0;

    public int getNumber() {
        return number;
    }

    public void increase(){
        this.number++;//不是原子操作,number++相当于3步
        System.out.println(number);
    }

    public static void main(String[] args) {
        final VolatileDemo vd = new VolatileDemo();
        for(int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                public void run() {
                    vd.increase();
                }
            }).start();
        }    //保证所有线程执行完成再执行输出
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number : " + vd.getNumber());
    }

}

  上述代码的输出结果很多时候都小于500,这是由于number++的非原子性操作导致的。也就是说A线程从主内存read到number修改后还没load到主内存中,这时B线程从主内存中也read到number,导致主内存中number有时候会被覆盖掉,因而输出结果会有小于500的情况。

  为了保证上述number++的原子性,可使用synchronized、ReentrantLock(java.util.concurrent.locks包下)、AtomicInterger(java.util.concurrent.atomic包下)三种方式实现。

  synchronized实现同步上面已介绍,这里不再累述。

  ReentrantLock方式如下:

public class VolatileDemo {
    private Lock lock = new ReentrantLock();
    private volatile int number = 0;

    public int getNumber() {
        return number;
    }

    public void increase(){
        lock.lock();
        try {
            this.number++;//不是原子操作,number++相当于3步
        }finally {
            lock.unlock();//保证锁的释放
        }
        System.out.println(number);
    }

    public static void main(String[] args) {
        final VolatileDemo vd = new VolatileDemo();
        for(int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                public void run() {
                    vd.increase();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number : " + vd.getNumber());
    }

}

  运行上述代码,我们发现结果为确定的500。

  注意:共享数据的访问权限都必须定义为private

     java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生

     java中对共享数据操作的并发控制是采用加锁技术

  1.3、通过重入锁实现线程同步

    参考1.2中volatile原子性问题解决办法ReentrantLock方式的代码。

  1.4、通过局部变量实现线程同步

  ava.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。引用代码

public class BankTest {
    public class Bank{
        //使用ThreadLocal类管理共享变量account
        private ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
            @Override
            protected Integer initialValue(){
                return 100;
            }
        };
        public void save(int money){
            account.set(account.get()+money);
        }
        public int getAccount(){
            return account.get();
        }
    }

    class NewThread implements Runnable {
        private Bank bank;

        public NewThread(Bank bank) {
            this.bank = bank;
        }

        public void run() {
            for (int i = 0; i < 10; i++) {
                // bank.save1(10);
                bank.save(10);
                System.out.println(i + "账户余额为:" + bank.getAccount());
            }
        }

    }

    /**
     * 建立线程,调用内部类
     */
    public void useThread() {
        Bank bank = new Bank();
        NewThread new_thread = new NewThread(bank);
        System.out.println("线程1");
        Thread thread1 = new Thread(new_thread);
        thread1.start();
        System.out.println("线程2");
        Thread thread2 = new Thread(new_thread);
        thread2.start();
    }

    public static void main(String[] args) {
        BankTest st = new BankTest();
        st.useThread();
    }

}

  1.5、通过阻塞队列实现线程同步

  前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步。(引用代码)

public class BlockingSynchronizedThread {
    /**
     * 定义一个阻塞队列用来存储生产出来的商品
     */
    private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
    /**
     * 定义生产商品个数
     */
    private static final int size = 10;
    /**
     * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程
     */
    private int flag = 0;

    private class LinkBlockThread implements Runnable {
        public void run() {
            int new_flag = flag++;
            System.out.println("启动线程 " + new_flag);
            if (new_flag == 0) {
                for (int i = 0; i < size; i++) {
                    int b = new Random().nextInt(255);
                    System.out.println("生产商品:" + b + "号");
                    try {
                        queue.put(b);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                for (int i = 0; i < size / 2; i++) {
                    try {
                        int n = queue.take();
                        System.out.println("消费者买去了" + n + "号商品");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
        LinkBlockThread lbt = bst.new LinkBlockThread();
        Thread thread1 = new Thread(lbt);
        Thread thread2 = new Thread(lbt);
        thread1.start();
        thread2.start();

    }

  1.6、通过原子变量实现线程同步

  需要使用线程同步的根本原因在于对普通变量的操作不是原子的,util.concurrent.atomic包中提供了创建了原子类型变量的工具类AtomicInteger 表可以用原子方式更新int的值。

public class VolatileDemo {
    private AtomicInteger number = new AtomicInteger(0);

    public AtomicInteger getNumber() {
        return number;
    }

    public synchronized void increase(){
        number.addAndGet(1);
        System.out.println(number);
    }

    public static void main(String[] args) {
        final VolatileDemo vd = new VolatileDemo();
        for(int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                public void run() {
                    vd.increase();
                }
            }).start();
        }
        //保证所有线程执行完成再执行输出
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number : " + vd.getNumber());
    }
}

  上述代码输出结果为确定的500。

2、数据交换

  由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

  1)通过构造方法传递数据

public class ParaTest extends Thread{

    private String name;

    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + " | " + name);
    }

    public ParaTest(String name){
        this.name = name;
    }

    public static void main(String[] args) {
        ParaTest p0 = new ParaTest("thread0");
        Thread t0 = new Thread(p0, "thread0");
        t0.start();
        ParaTest p1 = new ParaTest("thread1");
        Thread t1 = new Thread(p1, "thread1");
        t1.start();
    }
}

  当传递数据过多时,构造方法会显得特别臃肿,因此可以使用变量方法的方式。

  2)通过变量和方法传递数据

public class ParaTest extends Thread{

    private String names;

    public String getNames() {
        return names;
    }

    public void setNames(String names) {
        this.names = names;
    }

    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + " | " + getNames());
    }

    public static void main(String[] args) {
        ParaTest p0 = new ParaTest();
        p0.setNames("thread0");
        Thread t0 = new Thread(p0, "thread0");
        t0.start();
        ParaTest p1 = new ParaTest();
        p1.setNames("thread1");
        Thread t1 = new Thread(p1, "thread1");
        t1.start();
    }
}

  3)通过回调函数传递数据

  上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,这种情况可以使用回调函数方式。

public class ParaTest extends Thread{

    private String names;
    private int age;

    public String getNames() {
        return names;
    }

    public void setNames(String names) {
        this.names = names;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void progress(String name, int age) {
        System.out.println(name + " | " + age);
    }

    @Override
    public void run() {
       progress(getNames(), getAge());
    }

    public static void main(String[] args) {
        ParaTest p0 = new ParaTest();
        p0.setNames("thread0");
        p0.setAge(0);
        Thread t0 = new Thread(p0, "thread0");
        t0.start();
        ParaTest p1 = new ParaTest();
        p1.setNames("thread1");
        p1.setAge(1);
        Thread t1 = new Thread(p1, "thread1");
        t1.start();
    }
}

3、线程死锁

  所谓死锁: 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外部处理作用,它们都将无限等待下去。

  产生原因:

    1、系统资源不足,导致线程对资源的竞争引起

    2、进程的推进顺序不恰当

    3、资源分配不当

  死锁产生的条件:

    1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

    2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

    3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

    4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

  一个死锁的例子

public class DeadLockTest  implements Runnable {
    private int flag = 1;
    private static Object obj1 = new Object(), obj2 = new Object();

    public void run() {
        System.out.println("flag=" + flag);
        if (flag == 1) {
            synchronized (obj1) {
                System.out.println("我已经锁定obj1,休息0.5秒后锁定obj2去!");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (obj2) {
                System.out.println("我已经锁定obj2,休息0.5秒后锁定obj1去!");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1) {
                    System.out.println("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLockTest run01 = new DeadLockTest();
        DeadLockTest run02 = new DeadLockTest();
        run01.flag = 1;
        run02.flag = 0;
        Thread thread01 = new Thread(run01);
        Thread thread02 = new Thread(run02);
        System.out.println("线程开始喽!");
        thread01.start();
        thread02.start();
    }
}

  解决死锁的办法

  1、预防死锁:设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个

  2、避免死锁:而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态

  3、检测和解除死锁:先检测再解除。此方法允许系统在运行过程中发生死锁,但可通过系统所设置的检测机构(检测方法包括定时检测、效率低时检测、进程等待时检测等。),及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,采取适当措施,从系统中将已发生的死锁清除掉。

4、synchronized和volatile的比较

  volatile不需要加锁,比synchronized更轻量级,不会阻塞线程

  从内存可见性角度讲,volatile读相当于加锁,写相当于解锁

  synchronized即能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

Java多线程学习(吐血超详细总结)

Java线程面试题 Top 50

java并发之原子性、可见性、有序性

java笔记--关于线程同步(7种同步方式)

时间: 2024-12-18 09:42:05

java从基础知识(十)java多线程(下)的相关文章

Java 接口基础知识

一.Java接口基础知识 1, Java语言不支持一个类有多个直接的父类(多继承),但可以实现(implements)多个接口,间接的实现了多继承. 2, 与Java接口相关的设计模式: 1, 定制服务模式 设计精粒度的接口,每个Java接口代表相关的一组服务,通过继承来创建复合接口 2, 适配器模式 当每个系统之间接口不匹配时,用适配器来转换接口 3, 默认适配器模式 为接口提供简单的默认实现 4, 代理模式 为Java接口的实现类创建代理类,使用者通过代理来获得实现类的服务 5, 标识类型模

什么才是java的基础知识?

近日里,很多人邀请我回答各种j2ee开发的初级问题,我无一都强调java初学者要先扎实自己的基础知识,那什么才是java的基础知识?又怎么样才算掌握了java的基础知识呢?这个问题还真值得仔细思考. 我做j2ee开发已经超过十载,作为过来人,心路历程估计和大家差不多.编码的前几年,很长一段时间觉得java简单,开发实现各种功能都很轻松,代码写起来根本不费劲(主要是因为写的代码都是一些功能业务逻辑).但同时自己心里明白,自己其实没有什么水平,自己这3,4年以来学懂的东西就那么多,其他人几个月就可以

Java Web基础知识之Filter:过滤一切你不想看到的事情

不要相信客户端, 所以做后端的人都应该铭记的事情.因为前端传过来的数据并不总是合法和有效的,所以后端是对访问资源的最后一道保护伞.之前我们在Spring中说到过AOP编程,AOP基础知识,它就可以在执行我们的方法之前进行一些预处理和验证来保护后端的资源.不难想到她的实现方式和本篇要说的过滤器的实现原理应该是相同的,都是通过Java的动态代理实现的(自己的理解). 在Java Web的开发中,过滤器用于拦截请求,并对ServletRequest对象进行处理,我们可以想到的,它可以用来验证权限.加密

Java Script基础(十) 访问样式表

Java Script基础(十) 访问样式表 动态控制样式表 在JavaScript中,有两种方式可以动态的改变样式属性,一种是使用style属性,另一种是使用样式的className属性.另外控制元素隐藏和显示使用display属性. 1.使用sytle属性 语法: 元素.style.样式属性="值"; 在JavaScript中使用CSS样式与在html中使用CSS少有不同,由于JavaScript中的-表示减号,因此如果样式属性名称中带有"-"则要省去,后面首字

Java Script基础(十二) 正则表达式

Java Script基础(十二) 正则表达式 一.正则表达式中常用的符号 虽然可以使用string函数来完成验证,但是这种验证方式不够严谨,而且编写比较麻烦.而正则表达式是一种描述字符模式的对象,由一些特殊的符号组成,其组成的字母模式用来匹配各种表达式.下表中列举出常用的符号和用法. 正则表达式中常用的符号和用法 符号 描述  /....../  代表一个模式的开始和结束.  ^  匹配字符串的开始   $  匹配字符串的结束  \s  任何空白字符   \S  任何非空白字符   \d  匹

黑马程序员——Java集合基础知识之Collection

集合基础知识--Collection Java中集合框架由常用的Collection接口和Map接口组成,而Collection接口又有两个子接口,是List接口和Set接口,常用的集合框架由这三个类组成. List接口的功能方法 List的使用最为简单,创建集合,通过add方法添加元素,get方法获取元素,通过迭代器获取元素.List接口存放的数据无序的,添加速度快,但是查询速度慢,因为查询的时候必须遍历,每次都重头开始,效率较低.常用实现类有ArrayList,LinkedList. Lis

java基础知识《JAVA 核心技术》学习笔记(一)

一:数据类型 (1)java整形: 类型                         存储要求 int                              4字节 short                          2字节 long                           8字节 byte                           1字节 (2)浮点类型 类型                         储存要求 float            

Java web基础总结十之—— jsp EL表达式

Java web基础总结十之-- jsp EL表达式 一.EL表达式简介 EL 是Expression Language的缩写.在jsp中有很大的作用,EL主要有以下一些作用:获取数据,替换JSP页面中的脚本表达式,以从各种类型的web域中检索java对象.获取数据.还可以执行运算,利用EL表达式可以在JSP页面中执行一些基本的关系运算.逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如 ${user==null}.还可以获取web开发常用的jsp的隐式对象,利用这些隐式对象,we

黑马程序员——Java集合基础知识之Map

Map概念 要同时存储两个元素Key和Value,他们之间有映射关系,每个键不能重复,每个键只能映射到一个值. 当数据之间存在映射关系的时候,考虑使用Map集合. Map常用方法 如果添加的键原来有值,后添加的值会覆盖前面的值,并返回之前的值.put会返回来先添加的值,后添加的值会覆盖原有的值. Map tm =new TreeMap(); tm.put (key, value);//MAP没有add tm.remove (key) ;//去除一个key和对应的value,若不存在key返回nu

ASP.NET Core 2.2 基础知识(十六) SignalR 概述

原文:ASP.NET Core 2.2 基础知识(十六) SignalR 概述 我一直觉得学习的最好方法就是先让程序能够正常运行,才去学习他的原理,剖析他的细节. 就好像这个图: 所以,我们先跟着官方文档,创建一个 SignalR 应用: https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-2.2&tabs=visual-studio 这个例子一共涉及到下面几个步骤: 自定义中心 ChatH