创建和启动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

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

java 线程安全(初级)的更多相关文章

  1. Java线程并发:知识点

    Java线程并发:知识点   发布:一个对象是使它能够被当前范围之外的代码所引用: 常见形式:将对象的的引用存储到公共静态域:非私有方法中返回引用:发布内部类实例,包含引用.   逃逸:在对象尚未准备 ...

  2. Java线程的概念

    1.      计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行:当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了. 缓 ...

  3. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  4. 细说进程五种状态的生老病死——双胞胎兄弟Java线程

    java线程的五种状态其实要真正高清,只需要明白计算机操作系统中进程的知识,原理都是相同的. 系统根据PCB结构中的状态值控制进程. 单CPU系统中,任一时刻处于执行状态的进程只有一个. 进程的五种状 ...

  5. 【转载】 Java线程面试题 Top 50

    Java线程面试题 Top 50 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员 的欢迎.大多数待遇丰厚的J ...

  6. 第24章 java线程(3)-线程的生命周期

    java线程(3)-线程的生命周期 1.两种生命周期流转图 ** 生命周期:**一个事物冲从出生的那一刻开始到最终死亡中间的过程 在事物的漫长的生命周期过程中,总会经历不同的状态(婴儿状态/青少年状态 ...

  7. 第23章 java线程通信——生产者/消费者模型案例

    第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...

  8. 第22章 java线程(2)-线程同步

    java线程(2)-线程同步 本节主要是在前面吃苹果的基础上发现问题,然后提出三种解决方式 1.线程不安全问题 什么叫线程不安全呢 即当多线程并发访问同一个资源对象的时候,可能出现不安全的问题 对于前 ...

  9. 第21章 java线程(1)-线程初步

    java线程(1)-线程初步 1.并行和并发 并行和并发是即相似又有区别: 并行:指两个或者多个事件在同一时刻点发生. 并发:指两个或多个事件在同一时间段内发生 在操作系统中,并发性是指在一段事件内宏 ...

随机推荐

  1. spring整合mybatis报.UnsatisfiedDependencyException错误

    tomcat启动报org.springframework.beans.factory.UnsatisfiedDependencyException:错误 org.springframework.bea ...

  2. MySQL表关系--外键

    一.外键前戏 如果我们把所有的信息都记录在一张表中会带来的问题: 1.表的结构不清晰 2.浪费磁盘空间 3.表的扩展性极差 所以我们要把这种表拆成几张不同的表,分析表与表之间的关系. 确定表与表之间的 ...

  3. 基于FPGA Manager的Zynq PL程序写入方案

    本文主要描述了如何在Linux系统启动以后,在线将bitstream文件更新到ZYNQ PL的过程及方法.相关内容主要译自xilinx-wiki,其中官网给出了两种方法,分别为Device Tree ...

  4. DevExpress中GridColumnCollection实现父子表数据绑定

    绑定数据: 父表: DataTable _parent = _dvFlt.ToTable().Copy(); 子表: DataTable _child = _dvLog.ToTable().Copy( ...

  5. jedis异常:Could not get a resource from the pool

    前几天公司后端系统出现了故障,导致app多个功能无法使用,查看日志,发现日志出现较多的redis.clients.jedis.exceptions.JedisConnectionException: ...

  6. c# sharepoint client object model 客户端如何创建中英文站点

    c# sharepoint client object model 客户端如何创建中英文站点 ClientContext ClientValidate = tools.GetContext(Onlin ...

  7. installer

    if (args.Length == 0) { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new MyServi ...

  8. 2019-07-23 php魔术方法

    本文对一些php中的魔术方法进行总结,魔术方法顾名思义就是具备神奇功能的方法(function).魔术方法通常在某些特定情况下自动触发,不能用实例化变量名->方法名()来主动触发.不同的魔术方法 ...

  9. 用vue-cli搭建vue项目

    首先需要明确的是:Vue.js 不支持 IE8 及其以下 IE 版本,一般用与移动端,基础:开启最高权限的DOS命令(否则会出现意外的错误提示) 一.安装node.js,检测版本node -v,还要检 ...

  10. GO实现Cron解析和定时任务

    Go的Cron表达式解析库:github.com/gorhill/cronexpr 核心类型和方法 // 表达式对象 expr *cronexpr.Expression // 解析cron表达式 ex ...