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,否则什么都不做。

看一段伪代码:

原文地址:https://www.cnblogs.com/alimayun/p/10903209.html

时间: 2024-08-06 13:13:37

java并发系列(一)-----多线程简介、创建以及生命周期的相关文章

死磕 java线程系列之线程池深入解析——生命周期

摘自:https://www.cnblogs.com/tong-yuan/p/11748887.html (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 上一章我们一起重温了下线程的生命周期(六种状态还记得不?),但是你知不知道其实线程池也是有生命周期的呢?! 问题 (1)线程池的状态有哪些? (2)各种状态下对于任务队列中的任务有何影响? 先上源码 其实,在我们讲线程池体

Java并发系列[5]----ReentrantLock源码分析

在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

17、Java并发性和多线程-避免死锁

以下内容转自http://ifeve.com/deadlock-prevention/: 在有些情况下死锁是可以避免的.本文将展示三种用于避免死锁的技术: 加锁顺序 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生. 如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生.看下面这个例子: Thread 1: lock A lock B Thread 2: wait for A lock C (when A locked) Thread 3: wait for A

Java 并发和多线程(一) Java并发性和多线程介绍[转]

作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行. 随着多任务对软件开发者带来的

【小白的java成长系列】——多线程初识(多人买票问题)

本来这节内容是要到后面来说的,因为最近在弄并发的问题,推荐一本书<java并发编程实战>,深入的讲解了多线程问题的.本人最近也刚好在看这本书,还不错的~ 多线程的相关概念,就不用说了的,自己可以去网上查找,有一大堆关于它的讲解~ 先来看看买票的程序: package me.javen.thread.one; public class TicketDemo { public static void main(String[] args) { // 使用Thread类的方式 // TicketTh

Java并发编程:线程的创建

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析

学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

Java并发编程之多线程同步

线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他线程都能够得到这个最新数据.下面我们就来了解Java一些基本的同步机制. Java提供了一种稍弱的同步机制即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.然而,在访问volatile变量时不会执行加锁操作