java — 线程(二)

死锁

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

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

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日

相关文章

  • 详解SpringMVC学习系列之国际化

    详解SpringMVC学习系列之国际化 在SpringMVC中,我们可以使用国际化来支持多语言。本文将介绍如何在SpringMVC中使用国际化。 配置国际化资源文件 首先,我们需要在SpringMVC中配置国际化资源文件。我们可以在application.properties文件中添加以下配置: spring.messages.basename=i18n/m…

    Java 2023年5月17日
    00
  • Java中的异常处理如何提高程序可移植性?

    Java中的异常处理机制能够大大提高程序的可移植性,因为它能够保证对于任何一个程序,在任何一个平台上运行时都能够有相同的表现。 以下是提高Java程序可移植性的三个方法: 1.使用异常处理机制 在Java中,异常处理机制是一个十分重要的特性。通过在程序中使用异常处理,我们可以在程序出错时及时地捕捉并处理异常,从而保证程序能够正常地运行。正是因为有了异常处理机…

    Java 2023年4月27日
    00
  • vue.js数据响应式原理解析

    Vue.js数据响应式原理解析 Vue.js是一个极易上手,功能强大的Javascript框架,它的核心就是数据响应式系统。在Vue.js中,我们可以轻松的绑定数据和视图,而这一切都得益于Vue.js的数据响应式系统。在本篇文章中,我们将深入剖析Vue.js数据响应式原理。 数据响应式系统란? Vue.js的数据响应式系统简单来说,就是一种将ViewMode…

    Java 2023年5月23日
    00
  • Java MongoDB数据库连接方法梳理

    Java MongoDB数据库连接方法梳理 简介 MongoDB是一种开源、高性能、非关系型文档型数据库。由于其高效性和强大的原生查询语言,越来越多的企业和开发者开始选择MongoDB作为他们的首选数据库。本篇文章将介绍如何在Java应用程序中连接MongoDB数据库。 步骤 1. 安装MongoDB 在连接MongoDB之前,我们需要先安装MongoDB。…

    Java 2023年5月20日
    00
  • Java字符串中指定部分反转的三种方式

    以下是Java字符串中指定部分反转的三种方式的完整攻略,希望对您有所帮助。 方式一:使用StringBuffer反转指定部分字符串 通过Java自带的StringBuffer类可以方便地反转指定部分字符串。具体实现过程如下: 将原始字符串转换为StringBuffer对象,以便进行修改 使用StringBuffer的reverse()方法反转指定的子串 将修…

    Java 2023年5月27日
    00
  • springboot如何为web层添加统一请求前缀

    为web层添加统一请求前缀可以通过Spring Boot提供的@RestControllerAdvice注解来实现,具体步骤如下: 步骤1:添加@RestControllerAdvice注解 在包含@Controller注解的基础类上添加@RestControllerAdvice注解,如下所示: @RestControllerAdvice public cl…

    Java 2023年6月16日
    00
  • javascript中this的用法实践分析

    JavaScript中this的用法实践分析 在JavaScript中使用this是一个常见的问题,它可以在不同的情况下指向不同的变量。因此,在编写JavaScript代码时,正确地理解并使用this非常重要。 什么是this this是一个关键字,它表示当前执行代码的对象。但它不是常规的变量,而是在函数被调用时才被赋值。也就是说,this关键字在程序运行时…

    Java 2023年5月26日
    00
  • Java工程如何打印程序日志过程解析

    下面我将详细讲解“Java工程如何打印程序日志过程解析”的完整攻略。 什么是程序日志 程序日志是指在程序运行过程中对程序行为进行记录的信息,包括但不限于程序运行错误、程序调试信息、程序状态等。 在Java工程中,常见的日志工具有Log4j、Logback等,它们将程序打印的日志信息输出到控制台、文件等位置,方便程序员了解程序的运行状态及定位程序错误。 日志级…

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