java — 线程(二)

yizhihongxing

死锁

死锁是指两个或两个以上的线程在执行过程中,由于竞争同步锁而产生的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁。

死锁的案例 : 同步代码块的嵌套
创建锁对象:

public class Lock {
    public static final Lock lockA = new Lock();
    public static final  Lock lockB = new Lock();
}

测试类:

public class DeadLockTest {
    public static void main(String[] args) {
        while(true){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (Lock.lockA){
                        System.out.println("getlockA...");
                        synchronized (Lock.lockB){
                            System.out.println("getlockB...");
                        }
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (Lock.lockB){
                        System.out.println("getlockB...");
                        synchronized (Lock.lockA){
                            System.out.println("getlockA...");
                        }
                    }
                }
            }).start();
        }
    }
}

生产者与消费者

创建2个线程,一个线程表示生产者,另一个线程表示消费者

import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        Object o = new Object();
        // 生产者
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (o) {
                        if (list.size() > 0) {
                            try {
                                o.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        list.add("aaaa");
                        System.out.println(list);
                        // 唤醒消费线程
                        o.notify();
                    }
                }
            }
        }).start();

        // 消费者
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (o) {
                        if (list.size() == 0) {
                            try {
                                o.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        list.remove(0);
                        System.out.println(list);
                        o.notify();
                    }
                }
            }
        }).start();

    }
}

线程方法sleep和wait的区别

  • sleep()是Thread类静态方法,不需要对象锁。
  • wait()方法是Object类的方法,被锁对象调用,而且只能出现在同步中。
  • 执行sleep()方法的线程不会释放同步锁。
  • 执行wait()方法的线程要释放同步锁,被唤醒后还需获取锁才能执行。

案例性能问题

wait()方法和notify()方法, 本地方法调用OS的功能,和操作系统交互,JVM找OS,把线程停止. 频繁等待与唤醒,导致JVM和OS交互的次数过多.

Condition接口

java.util.concurrent.locks.Condition 是一个接口类, 因此要使用其实现类创建对象
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象
以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)
其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用

// Condition常用方法:
public void await() // 线程等待
public void signal() // 唤醒一个等待的线程
public void singalAll() // 唤醒所有等待的线程
// 使用其实现类 ReentrantLock 的 newCondition方法获取 Condition
public Condition newCondition()
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 创建Lock
        Lock l = new ReentrantLock();

        // 获取 Condition 对象
        Condition con = l.newCondition();

        new Thread(new Runnable() {
            @Override
            public void run() {
                l.lock();
                System.out.println("开始等待");
                try {
                    con.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    l.unlock();
                }
            }
        }).start();

        Thread.sleep(2000);

        System.out.println("准备唤醒");
        l.lock();
        con.signal();
        l.unlock();
    }
}

Condition接口方法和Object类方法比较

  • Condition可以和任意的Lock组合,也就是实现了线程的分组管理。
    • 一个线程的案例中,可以使用多个Lock锁,每个Lock锁上可以结合Condition对象
    • synchronized同步中做不到线程分组管理
  • Object类wait()和notify()都要和操作系统交互,并通知CPU挂起线程,唤醒线程,效率低。
  • Condition接口方法await()不和操作系统交互,而是让线程释放锁,并存放到线程队列容器中,当被signal()唤醒后,从队列中出来,从新获取锁后在执行。
  • 因此使用Lock和Condition的效率比Object要快很多

生产者和消费者案例改进

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OverWriteWakeUpWaiting {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Lock l = new ReentrantLock();

        // 对线程进行分组管理
        Condition con1 = l.newCondition(); // 生产线程, 对象监视器
        Condition con2 = l.newCondition(); // 消费线程, 对象监视器

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    l.lock();
                    if (list.size() > 0) {
                        try {
                            con1.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    list.add("abc");
                    System.out.println(list);
                    con2.signal();
                    l.unlock();
                }
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    l.lock();
                    if (list.size() == 0) {
                        try {
                            con2.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    list.remove(0);
                    System.out.println(list);
                    con1.signal();
                    l.unlock();
                }
            }
        }).start();
    }
}

java并发编程的三大特性

原子性

原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行
下面具有原子性的操作有?

x = 1;
// x = 1,是一个单纯的赋值操作,满足原子性。
y=x;
// 实际是两个操作,分别是 读取x变量 ,将x赋值给y,这两个操作分别来看都是原子性的,但是合起来就不是了
x++;
// 实际是三个操作 ,先读取变量 ,在进行+1操作 ,再赋值给x,不满足原子性
x=x+1;
// 同上,不满足原子性

JAVA提供了原子性的技术保障有如下:

1、synchronized (互斥锁)
2、Lock(互斥锁)
3、原子类(CAS)
synchronized 和 Lock 都是通过互斥锁实现,即同一时刻只允许一个线程操作该变量,保障了原子性

原子类AtomicInteger

/*
    java.util.concurrent.atomic.AtomicInteger
    构造方法
        public AtomicInteger()创建具有初始值 0 的新 AtomicInteger。
        public AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger。
      方法
        int incrementAndGet()  以原子方式将当前值加 1。
        int getAndIncrement()  以原子方式将当前值加 1。

        int decrementAndGet()  以原子方式将当前值减 1。
        int getAndIncrement()  以原子方式将当前值减 1。


        int getAndAdd(int delta)  以原子方式将给定值与当前值相加。
        int addAndGet(int delta)  以原子方式将给定值与当前值相加。
        int get() 获取当前值。
 */
public class Test02 {
    public static void main(String[] args) {
        AtomicInteger  ai = new AtomicInteger(1);

        ai.incrementAndGet(); //++ai    2
        ai.getAndIncrement(); //ai++    3


        System.out.println(ai.get());// 3
        System.out.println(ai.getAndIncrement()); // 3
        System.out.println(ai.get()); // 4
        System.out.println(ai.incrementAndGet()); // 5
        System.out.println(ai.get()); //5     
    }
}

CAS无锁机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。当多条线程尝试使用CAS同时更新同一个变量时,只有其中一条线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是告知这次竞争失败,并可以再次尝试.
CAS的缺点:

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。

    CAS 通常是配合无限循环一起使用的,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
    
  2. 只能保证一个变量的原子操作。

    当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
    
  3. ABA问题。

    第一条线程获取到V位置的值  假设是 1
    第二条线程获取到V位置的值  也是1
    第一条线程cas成功 将值改为 0
    第一条线程又cas成功 将值改回 1
    这时第二条线程cas 发现值没变 还是1 cas成功   
    实际上当第二条线程cas时 V位置的值已经从 1-0-1
    这就是ABA问题 
    如何解决 每次获取V位置的值时,带上一个版本号.这样就可以避免ABA问题 java中AtomicStampedReference这个类在cas时就是通过版本号来解决的
    

可见性

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

public class Test {
    public static  boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1号线程启动....执行while循环");

                long num = 0;
                while(flag){
                    num++;
                }

                System.out.println(num);
            }
        }).start();

        Thread.sleep(2000);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("2号线程启动....修改flag的值为false,停止循环");
                flag = false;
            }
        }).start();

    }
}

通过如上案例 发现修改flag 的值并没有使循环结束

1.加锁,比如使用synchronized.

JMM关于synchronized的两条规定:
  1)线程解锁前,必须把共享变量的最新值刷新到主内存中
  2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值
public class Test {

    //使用同步方法获取flag的值
    public static synchronized boolean getFlag(){
        return flag;
    }

    public static  boolean flag = true;
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1号线程启动....执行while循环");
                long num = 0;
                    /*
                        线程调用getFlag方法时 先获取锁 也就是加锁
                        这时会先清空本地内存中共享副本的值,那么在使用值就需要从
                        主内存中重新获取 ,线程释放锁时,也就是解锁,会把共享变量flag
                        的值重新更新到主内存中
                     */
                    while(getFlag()){
                        num++;
                    }
                System.out.println(num);
            }
        }).start();

        Thread.sleep(2000);

        new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println("2号线程启动....修改flag的值为false,停止循环");
                flag = false;
            }
        }).start();
    }
}

2.使用volatile关键字保证可见性

public class Test {
    public static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1号线程启动....执行while循环");
                long num = 0;

                    while(flag){
                        num++;
                    }
                System.out.println(num);
            }
        }).start();
        Thread.sleep(2000);
        new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println("2号线程启动....修改flag的值为false,停止循环");
                flag = false;
            }
        }).start();
    }
}

volatile缓存可见性实现原理

底层实现主要是通过汇编lock前缀指令,会锁住这块区域的缓存,并写回主内存.

1.会将当前处理器缓存的行数据立即写回系统内存

2.这个写回内存的操作导致CPU的缓存该内存地址的数值失效(MESI协议)

volatile只能保证可见性,但是不能保证原子性,如果要保证原子性,请使用锁

有序性

一般来说,程序的执行顺序按照代码的先后顺序执行.但是处理器为了提高程序的效率,可能会对代码的执行顺序进行优化,它不保证程序中各个语句的执行先后顺序一致,但是保证程序的最终结果和代码顺序执行的结果一致.

int a = 10; 	//语句1
int b = 20;     //语句2
int c = 20;     //语句3
c= a + b;  //语句4

CPU可能会对没有依赖关系的语句进行重排,比如 2134,3124 但是不会对有依赖关系的数据进行重排比如 3和4 改为4和3 这样就会对结果造成影响.这种重排对单线程是没有任何影响的,但是如果是多线程就可能会出现问题.

验证CPU是否会进行指令重排:

public class Test {

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

        for (int i = 0; i < 500000; i++) {
            Test.State state = new Test.State();

            ThreadA t1 = new ThreadA(state);
            ThreadB t2 = new ThreadB(state);

            t1.start();
            t2.start();

        }

    }


    static class ThreadA extends Thread{
        private final Test.State  state;
        ThreadA(Test.State state){
            this.state =state;
        }


        public void run(){
            state.a=1;
            state.b=1;
            state.c=1;
            state.d=1;


        }

    }
    static class ThreadB extends Thread{
        private final Test.State  state;
        ThreadB(Test.State state){
            this.state =state;
        }


        public void run(){

            if( state.b== 1 && state.a ==0){
                System.out.println("b= " + state.b);
            }

            if(state.c == 1 &&(state.b==0|| state.a ==0)){
                System.out.println("c = " + state.c);
            }

            if(state.d==1 &&(state.a==0||state.b==0||state.c==0)){
                System.out.println("d " + state.d);
            }
        }

    }

    static  class  State{
        int a = 0;
        int b = 0;
        int c = 0;
        int d = 0;
    }
}
/*
c = 1
说明,CPU进行了重排,让c在b或者a前面进行了赋值.
改变顺序可能导致执行结果不同,因此需要禁止重排序。
*/

使用volatile关键字后 就不会出现刚才的情况

static  class  State{
        volatile int  a = 0;
        volatile int b = 0;
        volatile int c = 0;
        volatile int d = 0;
 }

由此可见:volatile关键字有两个作用1.保证可见性.2禁止重排序

单例设计模式

设计模式 : 不是技术,是以前的人开发人员,为了解决某些问题实现的写代码的经验.
Java的设计模式有23种,分为3个类别,创建型,行为型,功能型
单例代表单个实例,保证一个类的对象永远只有一个!

饿汉式

优点: 简单 多线程下没有任何问题
缺点:

  • 当类加载时 对象就会被直接创建
  • 若不被使用 对象就白创建了
public class danliDemo1 {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Single1.getInstance());
                }
            }).start();
        }
    }
}
class Single1 {
    private static Single1 s = new Single1();

    public Single1() {
    }

    public static Single1 getInstance(){
        return s;
    }

}

懒汉式

优点: 延迟加载 什么时候调用方法 什么时候创建对象
缺点: 多线程时 代码有问题

public class danliDemo2 {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Single2.getInstance());
                }
            }).start();
        }
    }
}
class Single2 {
    private static Single2 s;
    public Single2() { }

    public static Single2 getInstance(){

        if (s == null) {
            s = new Single2();
        }
        return s;
    }
}

安全问题

一个线程判断完变量 s=null,还没有执行new对象,被另一个线程抢到CPU资源,同时有2个线程都进行判断变量,对象创建多次

性能问题

第一个线程获取锁,创建对象,返回对象. 第二个线程调用方法的时候,变量s已经有对象了,根本就不需要在进同步,不要在判断空,直接return才是最高效的.
双重的if判断,提高效率 Double Check Lock(DCL)
DCL双检查锁机制单例,效率高,线程安全,多线程操作原子性。

class Single2 {
    private static Single2 s;
    public Single2() { }

    public static Single2 getInstance(){

        if (s == null) {
            synchronized (Single2DCL.class) {
                if (s == null) {
                    s = new Single2DCL();
                }
            }
        }
        return s;
    }
}

面试题

DCL单例是否需要使用volatile关键字?
需要,单例的模式, 不使用volatile关键字,可能线程会拿到一个尚未初始化完成的对象(半初始化)

原文链接:https://www.cnblogs.com/paopaoT/p/17319628.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:java — 线程(二) - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • ES6 Symbol数据类型的应用实例分析

    ES6 Symbol 数据类型的应用实例分析 Symbol 是 ES6 新增的数据类型,用于表示独一无二的值。它经常被用于表示对象的私有属性,也可以用于定义对象的方法。本文将详细讲解 Symbol 数据类型的应用实例。 1. 定义对象的私有属性 JavaScript 中没有原生的私有属性的概念,但是使用 Symbol 数据类型可以模拟出私有属性的效果。下面是…

    Java 2023年5月26日
    00
  • 详解Spring MVC如何测试Controller(使用springmvc mock测试)

    以下是关于“详解Spring MVC如何测试Controller(使用springmvc mock测试)”的完整攻略,其中包含两个示例。 详解Spring MVC如何测试Controller(使用springmvc mock测试) Spring MVC是一个基于Java的Web框架,它可以帮助我们快速开发Web应用程序。在开发过程中,我们需要对Control…

    Java 2023年5月17日
    00
  • CSS变量实现主题切换的方法

    下面我将详细讲解CSS变量实现主题切换的方法的完整攻略。 什么是CSS变量? CSS变量也称为自定义属性,是一种定义在CSS规则中的、可重复使用的值。与常规的CSS属性不同,CSS变量以双减号(–)开头,并可以在整个样式表的范围内使用。 实现简介 利用CSS变量实现主题切换的方法主要包括以下几个步骤: 定义多套主题色进行切换; 使用CSS变量将主题色应用到…

    Java 2023年6月15日
    00
  • 详解Jvm中时区设置方式

    我来详细讲解一下“详解Jvm中时区设置方式”的完整攻略。 什么是Jvm中的时区 Jvm是一种Java虚拟机,它是运行Java程序的基础。在Java程序中,时间是一个非常重要的概念,因此时区是一个必不可少的因素。Jvm中的时区设置可以控制Java程序使用的时间和日期格式。 Jvm中的时区设置方式 Jvm中的时区设置有三种方式,分别为: 1. 系统默认时区 Jv…

    Java 2023年5月20日
    00
  • java图论弗洛伊德和迪杰斯特拉算法解决最短路径问题

    Java图论:弗洛伊德和迪杰斯特拉算法解决最短路径问题 在图论中,最短路径问题是指在一张图中,从起始点到终点的所有路径中,具有最小路径权值的路径。本文将介绍Java语言中如何使用弗洛伊德和迪杰斯特拉算法解决最短路径问题。 弗洛伊德算法 弗洛伊德算法(Floyd算法)是一种通过动态规划解决所有最短路径的算法。该算法的时间复杂度为O(n^3),因此对于大型图而言…

    Java 2023年5月19日
    00
  • Java的Struts框架报错“NullActionForwardException”的原因与解决办法

    当使用Java的Struts框架时,可能会遇到“NullActionForwardException”错误。这个错误通常由以下原因之一起: 配置错误:如果配置文件中没有正确配置,则可能会出现此。在这种情况下,需要检查文件以解决此问题。 转发名称:如果转发名称不正确,则可能出现此。在这种情况下,需要检查转发名称以解决此问题。 以下是两个实例: 例 1 如果配置…

    Java 2023年5月5日
    00
  • java实现2048小游戏(含注释)

    Java实现2048小游戏(含注释)–完整攻略 一、实现思路 绘制游戏界面 完成键盘监听事件,监测用户按键,向左移动、向右移动、向上移动、向下移动 随机生成数字2或4 判断游戏是否结束,判断游戏是否胜利 将游戏界面进行优化 统计游戏分数 二、实现细节 1. 绘制游戏界面 2048的游戏界面是一个4×4的矩阵,我们需要用JPanel布局来实现。将该矩阵分成1…

    Java 2023年5月18日
    00
  • JAVA实战项目实现客户选购系统详细流程

    JAVA实战项目实现客户选购系统详细流程攻略 系统需求分析 客户选购系统是一个基于Web的在线应用程序。通过该系统客户可以在网上浏览商品并进行购买。系统需要满足以下需求: 提供商品浏览功能,客户可以浏览商品分类和商品详细信息。 提供购物车管理功能,客户可以将商品加入购物车,修改购物车中商品数量,删除购物车中商品等。 提供订单管理功能,客户可以查看自己的订单、…

    Java 2023年5月30日
    00
合作推广
合作推广
分享本页
返回顶部