java并发/多线程系列——线程安全篇

     2022-04-24     233

关键词:

创建和启动Java线程

Java线程是个对象,和其他任何的Java对象一样。线程是类的实例java.lang.Thread,或该类的子类的实例。除了对象之外,java线程还可以执行代码。

创建和启动线程

在Java中创建一个线程是这样完成的:

 Thread thread = new Thread();

要启动Java线程,您将调用其start()方法,如下所示:

thread.start();

此示例不指定要执行的线程的任何代码。启动后,线程将立即停止。

有两种方法来指定线程应该执行什么代码。第一个是继承Thread类并覆盖run()方法。第二种方法是实现Runnable (java.lang.Runnable对 Thread构造函数)的接口,这两个方法都在下面。

继承Thread

创建线程的第一种方法是创建Thread的子类并覆盖该run()方法。当执行start()方法后,会另起一个线程调用该run()方法。以下是创建Java Thread子类的示例:

public class MyThread extends Thread {

  public void run(){
     System.out.println("MyThread running");
  }
}

 

启动线程:

MyThread myThread = new MyThread();
myTread.start();

start()一旦线程启动, 该调用将返回。它不会等到run()方法完成。该run()方法将像执行不同的CPU一样执行。当run()方法执行时,它将打印出文本“MyThread running”。

你也可以创建一个这样的匿名子类的Thread

复制代码
Thread thread = new Thread(){
  public void run(){
    System.out.println("Thread Running");
  }
}

thread.start();
复制代码

 

此示例将打印出文本“Thread running” 。

实现Runnable接口

创建线程的第二种方法是创建一个java.lang.Runnable接口的实现类。该实现类可以通过一个被执行Thread运行。

示例:

public class MyRunnable implements Runnable {

  public void run(){
     System.out.println("MyRunnable running");
  }
}

 

要执行run()方法,需要创建拥有MyRunnable实例的Thread对象,如下:

Thread thread = new Thread(new MyRunnable());
thread.start();

 

当线程启动时,它将调用MyRunnablerun()方法。上面的例子将打印出文本“MyRunnable running”。

您还可以创建一个匿名实现Runnable,像这样:

复制代码
Runnable myRunnable = new Runnable(){

   public void run(){
      System.out.println("Runnable running");
   }
 }

Thread thread = new Thread(myRunnable);
thread.start();
复制代码

 

继承Thread父类还是实现Runnable接口?

这两种方法没有说哪一种是最好的,这两种方法都有效。我个人而言,我更喜欢使用Runnable,并将实现的一个实例移交给一个Thread实例。当Runnable通过线程池执行该操作时,Runnable 实例很容易列入队列中,直到来自池的线程空闲时再运行run()方法。而Thread的子类就难于实现。

有时你可能需要实现Runnable和子类Thread。例如,创建一个子类Thread可以执行多个Runnable。实现线程池时通常是这种情况。

常见的陷阱:调用run()而不是start()

当创建和启动一个线程时,一个常见的错误是调用run()方法而不是Threadstart(),像这样:

Thread newThread = new Thread(MyRunnable());
newThread.run(); //应该是start();

起初你可能不会注意到这样会发生错误,因为它Runnablerun()方法是像你预期的那样执行。但是,它不是刚刚创建的新线程执行。相反,该run()方法由创建线程的线程执行。换句话说,执行上述两行代码的线程。要由新创建的线程去调用MyRunnable实例的run()方法,你必须通过newThread.start()去调用。

线程名称

创建Java线程时,可以给它一个名称。该名称可以帮助您区分不同的线程。例如,如果多个线程写入System.out,它可以方便地查看哪个线程写了文本。两种不同的创建线程方式的例子:

复制代码
Thread thread = new Thread("New Thread"){
    public void run(){
      System.out.println("run by:" + getName());
   }
};


thread.start();
System.out.println(thread.getName());
复制代码

 

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");

thread.start();
System.out.println(thread.getName());

 

但是请注意,由于MyRunnable类不是 Thread的子类,所以它无法通过执行getName()去获取线程名字。

获取当前线程

Thread.currentThread()方法能够返回当前线程的实例,这样你就可以获取到当前线程中你想得到的东西。例如,您可以获取当前执行代码的线程的名称,如下所示:

Thread thread = Thread.currentThread();
String threadName = Thread.currentThread().getName();

Java Thread示例

这是一个小例子。首先打印执行该main()方法的线程的名称。该线程由JVM分配。然后它启动10个线程,并给它们全部一个数字作为name("" + i)。然后每个线程将其名称输出,然后停止执行。

复制代码
public class ThreadExample {
    
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        for(int i = 0; i <10; i ++){
          new Thread("" + i){
            public void run(){
              System.out.println("Thread:" + getName() +"running");
            }
          }.start();
        }
    }

}
复制代码

请注意,即使线程按顺序(1,2,3...)启动,它们可能不会按顺序执行,这意味着线程0可能不是第一个用System.out把线程名称输出的。这是因为线程原则上是并行执行而不是顺序执行的。JVM操作系统决定执行线程的顺序。每次运行结果会不相同,因此这个顺序不一定是他们的执行顺序。

竞争条件(Race Conditions)和临界区(Critical Sections)

竞争条件是在临界区内可能出现的一种特殊情况。临界区是一种轻量级机制,在某一时间内只允许一个线程执行某个给定代码段。

当多线程在临界区执行时,执行结果可能会根据线程执行的顺序而有所不同,临界区被称为包含竞争条件。竞争条件一词来自比喻,即线程正在通过临界区时进行赛跑,而竞争的结果影响了执行临界区的结果。

这可能听起来有点复杂,所以我将在以下部分详细阐述竞争条件和临界区。

临界区

在同一应用程序中运行多个线程本身不会导致问题。当多个线程访问相同的资源时,就会出现问题。例如多个线程同时访问相同的内存(变量,数组或对象),系统(数据库,Web服务等)或文件。

事实上,只有一个或多个线程写入这些资源时才会出现问题。只要资源不变,可以安全地让多个线程读取相同的资源。

以下是一个临界区的代码示例,如果多个线程同时执行,则可能会失败:

复制代码
public class Counter {

   protected long count = 0;

   public void add(long value){
       this.count = this.count + value;
   }
}
复制代码

想象一下,如果两个线程A和B正在同一个Counter类的实例上执行add方法。没有办法知道操作系统何时在两个线程之间切换。该add()方法中的代码不会作为Java虚拟机的单个原子指令执行。相反,它作为一组较小的指令执行,类似于此:

  1. 把这个记录从内存读入注册表。
  2. 添加值进行注册。
  3. 写入寄存器到内存

观察以下的线程A和B的混合执行会发生什么:

复制代码
 this.count = 0;

A:把这个记录读入一个寄存器(0)
B:将此记录读入注册表(0)
B:添加值2进行注册
B:将寄存器值(2)写入内存。this.count现在等于2
A:添加值3进行注册
A:将寄存器值(3)写入内存。this.count现在等于3
复制代码

 

两个线程想要将值2和3添加到计数器。因此,两个线程完成执行后的值应该是5。然而,由于两个线程同时执行,所以结果会有所不同。

在上面列出的执行顺序示例中,两个线程从内存中读取值0。然后,他们将它们的个人值2和3添加到值中,并将结果写回内存。而不是5,剩下的值 this.count将是最后一个线程写入其值的值。在上面的情况下,它是线程A,但也可能是线程B.

临界区的竞争条件

上例中的add()方法就包含临界区,当多个线程执行此临界区时,会发生竞争条件。

多个线程竞争相同资源时,其中访问资源的顺序是重要的,称为竞争条件。导致竞争条件的代码部分称为临界区。

防止竞争条件

为了防止发生竞争条件,您必须确保临界区作为原子命令执行。这意味着一旦一个线程正在执行它,就不能有其他线程可以执行它,直到第一个线程离开临界区。

临界区的竞争条件可以通过适当的线程同步来避免。可以使用Java代码的同步块来实现线程同步。线程同步也可以使用其他同步结构(如锁或原子变量,如java.util.concurrent.atomic.AtomicInteger)来实现。

复制代码
public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;
    
    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
            this.sum2 += val2;
        }
    }
}
复制代码

然而,由于两个和变量是相互独立的,所以您可以将它们的求和分解为两个单独的同步块,如下所示:

复制代码
public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;

    private Integer sum1Lock = new Integer(1);
    private Integer sum2Lock = new Integer(2);

    public void add(int val1, int val2){
        synchronized(this.sum1Lock){
            this.sum1 += val1;   
        }
        synchronized(this.sum2Lock){
            this.sum2 += val2;
        }
    }
}
复制代码

现在两个线程可以同时执行该add()方法。两个同步块在不同的对象上同步,因此两个不同的线程可以独立执行两个块。这样线程将就有较少的等待去执行add()方法。

这个例子当然很简单。在现实生活中的共享资源中,临界区的分解可能会更复杂一些,并且需要更多的分析执行顺序的可能性。

线程安全和共享资源

多线程同时安全地调用被称为线程安全。如果一段代码是线程安全的,那么它不包含任何竞争条件。竞争条件仅在多个线程更新共享资源时发生。因此,重要的是要知道什么共享资源会被多线程同时执行。

局部变量

局部变量存储在每个线程自己的堆栈中。这意味着局部变量从不在线程之间共享。这也意味着所有本地变量基本上都是线程安全的。以下是本地变量的线程安全的示例:

public void someMethod(){

  long threadSafeInt = 0;

  threadSafeInt++;
}

本地对象的引用

引用本身不是共享的。但是,引用的对象不存储在每个线程的本地堆栈中,所有对象都存储在共享堆中。

如果本地创建的对象永远不会通过创建他的方法返回,那么它是线程安全的。实际上,只要没有让对象在方法之间传递后用于其他线程。

这是一个线程安全的本地对象的示例:

复制代码
public void someMethod(){

  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}
复制代码

 

上面这个例子,someMethod()这个方法没有将LocalObject传递出去,而是每个线程调用someMethod()都会创建一个新的LocalObject,并在自己的方法内部消化,所以这里是线程安全的。

对象成员变量

对象成员变量与对象一起存储在堆上。因此,如果两个线程调用同一对象实例上的方法,并且此方法更新该对象的成员变量,则该方法是线程不安全的。这是一个线程不安全的例子:

复制代码
public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }
}
复制代码

如果两个线程在同一个NotThreadSafe实例上同时调用add()方法那么它会导致竞争条件。例如:

复制代码
NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}
复制代码

但是,如果两个线程在不同的实例上同时调用add()方法 那么它们不会产生竞争条件。把上面的例子稍加修改:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

 

现在这两个线程都拥有自己的实例对象,所以他们调用add方法时不会互相干扰。代码没有竞争条件了。所以即使一个对象是线程不安全的,它仍然可以以不会导致竞争条件的方式运行。

线程控制逃离准则(The Thread Control Escape Rule)

为了确定你的代码对某个资源的访问是否是线程安全的,您可以使用“线程控制逃离准则”:

如果一个资源的创建、使用和回收都在同一个线程内完成的,并且从来没有逃离这个线程的控制域,那么该资源就是线程安全的

If a resource is created, used and disposed within the control of the same thread, and never escapes the control of this thread, the use of that resource is thread safe.

资源可以是任何形式的共享资源,如对象,数组,文件,数据库连接,套接字等。在Java中,你并不总是明确地回收某个对象,因此“回收”意味着对该对象的引用不再使用或者置为 null。

即使使用线程安全的对象,如果该对象指向一个共享资源,如文件或数据库,那么整个应用程序可能不是线程安全的。例如,如果线程1和线程2都创建自己的数据库连接,连接1和连接2,则使用每个连接本身是线程安全的。但是使用数据库的连接点可能不是线程安全的。例如,如果两个线程执行这样的代码:

check if record X exists
if not, insert record X

如果两个线程同时执行,并且他们正在检查的记录X恰好是相同的记录,那么就存在两个线程都进行插入的动作。那么这就是线程不安全的。

这种情况也可能发生在对文件或者其他共享资源的操作上。因此,一定要区分一个线程所控制的对象到底是资源本身还是指向资源的一个引用

线程安全和不变性

竞争条件只有在多个线程同时访问同一资源多个线程同时写入资源时才会发生。如果多线程读取相同的资源,那么竞争条件不会发生。

我们可以通过让共享对象不可变来确保多线程永远不会更新该对象,从而保证线程安全。例如:

复制代码
public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }
}
复制代码

注意ImmutableValue实例的属性value在构造函数中赋值。还要注意该没有提供setter方法。一旦ImmutableValue实例被创建,你不能改变它的属性value。当然,您可以使用该getValue()方法读它。

如果需要对ImmutableValue实例执行操作,可以通过操作得到返回一个新的实例来改变value的值,从而不改变原实例的value值。看下面例子会更加清晰:

复制代码
public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }

  
      public ImmutableValue add(int valueToAdd){
      return new ImmutableValue(this.value + valueToAdd);
      }
  
}
复制代码

 

请注意该add()方法返回的是一个新实例,而不是改变自身实例的value值。

实例的引用是线程不安全

非常重要的是,即使一个对象是不可变的,因此是线程安全,但该对象的引用可能不是线程安全的。例如:

复制代码
public class Calculator{
  private ImmutableValue currentValue = null;

  public ImmutableValue getValue(){
    return currentValue;
  }

  public void setValue(ImmutableValue newValue){
    this.currentValue = newValue;
  }

  public void add(int newValue){
    this.currentValue = this.currentValue.add(newValue);
  }
}
复制代码

Calculator类持有一个ImmutableValue实例的引用。但是Calculator可以通过方法setValue() 和add()方法来改变引用。因此,即使Calculator类在内部使用不可变对象ImmutableValue,但它本身不具有不变性,因此是线程不安全的。换句话说:ImmutableValue该类是线程安全的,但使用它的不是。当尝试通过不变性实现线程安全性时,需要牢记这一点。

为了使Calculator类线程安全,你可以将getValue(), setValue()add()方法加synchronized

 

from: https://www.cnblogs.com/bug-zhang/p/7624254.html

java并发编程系列多线程

1.什么是线程线程是CPU独立运行和独立调度的基本单位;2.什么是进程进程是资源分配的基本单位;3.线程的状态 新创建  线程被创建,但是没有调用start方法 可运行(RUNNABLE) 运行状态,由cpu决定是不是正在... 查看详情

java并发编程系列之一并发理论基础

Java并发编程系列之一并发理论基础本系列文章开始Java并发编程的进阶篇的学习,为了初学者对多线程的初步使用有基本概念和掌握,前置知识会对一些基础篇的内容进行介绍,以使初学者能够丝滑入戏。多线程学习,真正的难... 查看详情

linux线程安全篇(代码片段)

文章目录0、概述1、线程不安全举例1.1前提知识铺垫1.2场景模拟1.3代码模拟2、互斥2.1什么是互斥2.2互斥锁的原理&&特性2.3互斥锁的计数器如何保证原子性2.4互斥锁的接口2.4.1初始化接口2.4.2加锁接口2.4.3解锁接口2.4.4销毁接口... 查看详情

linux线程安全篇ⅰ(代码片段)

文章目录0、概述1、线程不安全举例1.1前提知识铺垫1.2场景模拟1.3代码模拟2、互斥2.1什么是互斥2.2互斥锁的原理&&特性2.3互斥锁的计数器如何保证原子性2.4互斥锁的接口2.4.1初始化接口2.4.2加锁接口2.4.3解锁接口2.4.4销毁接口... 查看详情

java——多线程高并发系列之创建多线程的三种方式(threadrunnablecallable)(代码片段)

...xff09;写在前面历时一个星期,终于整完了Java多线程高并发这个系 查看详情

java——多线程高并发系列之线程池(executor)的理解与使用(代码片段)

文章目录:写在前面Demo1(使用Executors创建线程池)Demo2(使用ThreadPoolExecutor创建线程池)关于ThreadPoolExecutor中的七大参数、四种拒绝策略线程池的执行策略写在前面可以以newThread(()->线程执行的任务).start();... 查看详情

java并发系列-----多线程简介创建以及生命周期(代码片段)

进程、线程与任务进程:程序的运行实例。打开电脑的任务管理器,如下:正在运行的360浏览器就是一个进程。运行一个java程序的实质是启动一个java虚拟机进程,也就是说一个运行的java程序就是一个java虚拟机进程。进程是程... 查看详情

java——多线程高并发系列之threadlocal的使用(代码片段)

文章目录:写在前面Demo1Demo2Demo3写在前面除了控制资源的访问外,还可以通过增加资源来保证线程安全。ThreadLocal主要解决为每个线程绑定自己的值。Demo1packagecom.szh.threadlocal;/***ThreadLocal的基本使用*/publicclassTest01//定义一... 查看详情

『图解java并发编程系列』10张图告诉你java并发多线程那些破事(代码片段)

目录线程安全问题活跃性问题性能问题有态度的总结 头发很多的程序员:『师父,这个批量处理接口太慢了,有什么办法可以优化?』架构师:『试试使用多线程优化』第二天头发很多的程序员:『师父&... 查看详情

linux线程安全篇ⅱ(代码片段)

...毁接口2.4条件变量夺命追问2.5条件变量的代码0、接上篇线程安全1、同步存在的必要性1.1样例引入有了互斥之后,为什么还要有同步呢?这个问题值得我们讨论,我们知道,互斥通过控制 查看详情

java——多线程高并发系列之arraylisthashsethashmap集合线程不安全的解决方案(代码片段)

...下图这样的异常信息:👇👇👇这是一个并发修改异常,首先ArrayList肯定是线程不安全的,产生这个异常的原因就是可能第一个线程刚进入ArrayList集合中要进行add操作时, 查看详情

java——多线程高并发系列之readwritelock读写锁(代码片段)

文章目录:写在前面Demo1(读读共享)Demo2(写写互斥)Demo3(读写互斥)写在前面synchronized内部锁与ReentrantLock锁都是独占锁(排它锁),同一时间只允许一个线程执行同步代码块,可以保证线程的... 查看详情

java并发系列终结篇:彻底搞懂java线程池的工作原理(代码片段)

多线程并发是Java语言中非常重要的一块内容,同时,也是Java基础的一个难点。说它重要是因为多线程是日常开发中频繁用到的知识,说它难是因为多线程并发涉及到的知识点非常之多,想要完全掌握Java的并发相... 查看详情

java多线程与并发:前置知识

目的这一系列的博文的目的是帮助自己对多线程的知识做一个总结,并且将Java中的多线程知识做一个梳理。尽量做到全面和和简单易懂。概念进程与线程进程是操作系统级别的,进程是操作系统分配资源的基本单位,一个进程... 查看详情

java——多线程高并发系列之lockreentrantlock(代码片段)

文章目录:写在前面说说synchronized和Lock的区别?Demo1(先演示一下锁的可重入性)Demo2(ReentrantLock的基本使用)Demo3(使用Lock锁同步不同方法中的代码块)Demo4(ReentrantLock锁的可重入性)Demo 查看详情

java多线程系列:一并发工具类的使用_2(countdownlatchcyclicbarriersemaphoreexchanger)(代码片段)

...,解析java多线程的各种技术及实现。随笔主要根据《java并发编程的艺术》一书作为参考。 本系列以使用为主要目的,本人理解有限,还望读者辩证采纳,没有过多涉及源码的讨论,重在初学者的使用,理解伪码。预备知识... 查看详情

java——多线程高并发系列之synchronized关键字(代码片段)

文章目录:写在前面Demo1(synchronized面对同一个实例对象)Demo2(synchronized面对多个实例对象)Demo3(synchronized面对一个publicstaticfinal常量)Demo4(synchronized同步代码块分别位于实例方法、静态方法中& 查看详情

java——多线程高并发系列之wait()notify()notifyall()interrupt()(代码片段)

文章目录:写在前面Demo1(不在同步代码块中调用wait方法,则产生java.lang.IllegalMonitorStateException运行时异常)Demo2(调用wait方法会使执行当前代码的线程进入等待状态)Demo3(notify方法会唤醒之前执行wai... 查看详情