java并发系列-----多线程简介创建以及生命周期

Posted alimayun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发系列-----多线程简介创建以及生命周期相关的知识,希望对你有一定的参考价值。

进程、线程与任务
进程:程序的运行实例。打开电脑的任务管理器,如下:

技术图片

正在运行的360浏览器就是一个进程。运行一个java程序的实质是启动一个java虚拟机进程,也就是说一个运行的java程序就是一个java虚拟机进程。进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位。

线程:是进程中可独立执行的最小单位,并且不拥有资源。进程相当于工厂老板,整个工厂的机器都是属于老板的,但是工厂里面的活都是由工人完成的。

任务:线程所要完成的计算就被称为任务,特定的线程总是执行特定的任务。

 

java线程的创建、启动与运行

1、线程创建方式

a、继承Thread

技术图片

b、实现Runnable接口

技术图片

线程的start()方法只能调用一次,多次调用会抛出IllegalThreadStateException异常。当调用start方法之后,由jvm决定何时运行线程的run(),当run方法执行结束(正常结束或抛出异常中止),线程的运行也就结束了。

 

2、线程的属性

技术图片

守护线程:通过daemon属性用于表示相应线程是否为守护线程。当所有用户线程都运行结束后,jvm才能正常停止。但是守护线程则不会影响jvm的正常停止,例如jvm中的垃圾回收就是守护线程。不过你要是通过kill命令直接干掉进程,那另说。

 

3、Thread类的常用方法

技术图片

 

线程生命周期

 技术图片

 

当多个线程对共享变量、共享资源进行访问的时候,很容易出现线程安全问题,那么解决思路就是将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,直到该线程访问结束后其他线程才能对其进行访问。锁(lock)就是基于这种思路实现的同步机制。

在java中,每个对象都包含一个锁,在多线程访问共享数据的时候,首先要获得共享对象的锁,在执行完之后需要释放锁,由其他线程获得。锁具有排他性,即一个锁一次只能被一个线程持有,这种锁被称为排他锁或互斥锁。

技术图片

 

1、锁分类

按照实现方式分,可以分为内部锁(synchronized)以及显式锁(java.concurrent.locks.ReentrantLock) 

2、锁的作用

保护共享数据以实现线程安全,包括保障原子性、可见性和有序性。

3、可重入性

一个线程在其持有一个锁的时候能否再次(或者多次)申请该锁。如果一个线程持有一个锁的时候还能够继续成功申请该锁,那么我们就称该锁是可重入的。

技术图片

技术图片

如何实现的?可重入锁可以被理解成是一个对象,对象中包含一个计数器,锁被一个线程持有时,计数器+1。

4、锁的开销

锁的开销包括锁的申请和释放所产生的开销、锁可能导致的上下文切换的开销。

 

synchronized

java平台中的任何一个对象都有唯一一个与之关联的锁,这种锁被称为监视器(Monitor)或者内部锁(Intrinsic Lock)。内部锁是一种排他锁,可以保障原子性、可见性和有序性。内部锁的实现方式就是通过synchronized关键字实现,可以修饰方法,也可以通过代码块的方式来实现线程安全。

1、内部锁调度(synchronized)

当有多个线程竞争被synchronized关键字修饰的方法或代码块时,会出现竞争,拿到锁的线程继续执行,没有获取锁的线程状态则变为blocked。jvm为每个内部锁分配一个入口集,记录等待需要获取锁的相应内部锁的线程,当获取到锁的线程释放锁之后,入口集中的一个任意线程会被jvm唤醒,得到再次申请锁的机会。内部锁仅支持非公平锁,后面要说的Lock则支持公平锁,公平锁的开销大于非公平锁。

 

Lock

1、ReentrantLock

显式锁是java.util.concurrent.locks.Lock接口的实例。java.util.concurrent.locks.ReentrantLock是Lock接口的默认实现类。

技术图片

使用示例:

技术图片

一般锁对象都会声明为private final

 

2、显示锁调度

ReentrantLock既支持公平锁也支持非公平锁,但是公平锁开销略大。可以想这么个场景,大家去银行取钱,然后在atm机排队,但是有些人不讲规矩,插队,这种就是非公平的;另外一种就是保安大叔在旁边看着,让大家保持秩序,但是这个大叔就得付出劳动,多个人力,开销自然大一点。

 

3、synchronized与lock的比较

a、灵活性
synchronized是基于代码块的锁,没有啥灵活性,粒度比较大,但是synchronized使用简单方便;
b、锁泄露
使用synchronized不用担心这个问题,当线程执行完同步代码块之后,jvm保证释放锁;但是lock如果开发人员忘记释放锁,则会出现锁泄露的问题,因此lock.unlock()一定要放在finally块中。
c、阻塞
获取锁自然会存在阻塞的情况,但是使用synchronized的时候,如果某一个线程迟迟不释放锁(可能由于代码错误导致),其他线程都得阻塞;使用lock则能够很好的解决这种问题,如下:

技术图片

lock.tryLock()还有另外一个重载方法,

技术图片

如果当前线程没有在指定时间内申请到(获取)相应的锁,那么tryLock方法就直接返回false。

 

volatile 

先来看一个经典的错误,双重锁检查,代码如下:

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();   // error
                }
            }
        }
        return uniqueSingleton;
    }
}

这是实现单例模式的一种写法,但是我已经标注了//error,为啥呢?因为new Singleton()这个操作不是原子的,我们来拆分一下:

objRef = allocate(Singleton.class);//在堆上分配内存空间
invokeConstructor(objRef);//调用构造器初始化对象
uniqueSingleton = objRef;//将这个对象引用赋给uniqueSingleton

这么一看好像还是没啥问题,但是jvm中有个东西叫做jit编译器,它的功能主要就是将java代码中执行比较频繁的代码直接编译成本地机器代码,并且为了优化性能,会发生指令重排序的现象,如下面这样:

objRef = allocate(Singleton.class);//在堆上分配内存空间
uniqueSingleton = objRef;//将这个对象引用赋给uniqueSingleton
invokeConstructor(objRef);//调用构造器初始化对象

先将对象引用给到uniqueSingleton,因此当其他线程判断此对象不为空之后,直接拿这对象进行操作,就有可能报错啦!因为构造器还没调呢,只是提前分配了内存空间。

下面则是正确的双重检查:

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

volatile关键字主要有两个作用

1、保证有序性

2、保证可见性

针对上面的这种现象,就是保障对象初始化按照 如下顺序执行,然后可见性则是保证线程可见。

objRef = allocate(Singleton.class);//在堆上分配内存空间
invokeConstructor(objRef);//调用构造器初始化对象
uniqueSingleton = objRef;//将这个对象引用赋给uniqueSingleton

那啥是可见性呢?其实本质上要从cpu说起,cpu的速度非常快,是内存的大约100倍左右,是硬盘的10000倍,因此cpu在执行完指令之后将结果返回到内存中时,这可急死人了,那咋办呢?在cpu中引入了缓存的概念,缓存比内存快,并且现代cpu中引入了好几层缓存,第一缓存、第二等等,cpu在执行完指令之后,将结果放在缓存中,并不是立马刷新到内存中,可能到这还是有点不大清楚。ok,再来看看jmm(java内存模型),java跨平台的根本原因就是jvm,jvm为了实现跨平台,避免开发者直接跟cpu等硬件打交道,因为你跟硬件打交道,很难做到跨平台,因为你的代码跟操作系统耦合在一起了,毕竟windows跟linux差距巨大,linux不同版本差距也大,jmm内存模型如下:

技术图片

每个线程都有自己的工作线程,当线程从主内存去获取某共享变量,比如A吧,然后放到自己的工作内存中,然后各个线程在自己的工作内存中去操作这个变量A,在某一时刻将结果刷新到主内存中(具体哪一时刻,由操作系统决定)。这时候volatile就发挥作用了,由两个作用:

  • 让各个线程不从工作内存中取这个共享变量;
  • 每个线程操作完这个变量之后,立马刷新到主内存中去

这样就保证了这个单例模式的正确性。那么volatile能跟synchronized一样保证原子性吗?答案是否定的。volatile无法保证原子性

 

CAS 

原理:CAS本质上是利用到了cpu的指令来保证线程安全,是一种乐观锁。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

看一段伪代码:

技术图片

 

以上是关于java并发系列-----多线程简介创建以及生命周期的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程系列:最全面的Java多线程学习概述

阿里面试系列Java线程的应用及挑战

Java多线程与并发——线程生命周期和线程池

Java多线程系列:线程的五大状态,以及线程之间的通信与协作

Java多线程系列:线程的五大状态,以及线程之间的通信与协作

Java并发编程系列之二线程基础