java多线程

来源:互联网 发布:数据库概念模式 编辑:程序博客网 时间:2024/06/06 08:42

基础概念

定义

进程:资源分配的最小单元,在java中就是一个程序;
线程:进程中可独立执行的子任务,是执行任务的最小单元,在JVM中,进程是线程的一个组件。
线程分为守护线程和用户线程。用户线程可以通过System.exit调用停止JVM的运行,也就是说在JVM停止之前,所有的用户线程必须先停止,而守护线程不会影响。

创建与运行

创建方式:1.继承java.lang.Thread类的实例;2.实现java.lang.Runnable接口
采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
运行:start方法而不是run方法

状态与上下文切换

状态(Thread.state):new,runnable,running,blocked,waiting,timed_waiting,terminated。
NEW:刚创建未启动,未执行start方法。
RUNNABLE:包含READY和RUNNING两个子状态,当调用yield方法时,或者线程调度器问题,相应的线程状态会有running转化成Ready。
BLOCKED:一个线程发起阻塞式IO或者试图获取一个由其他线程持有的锁时,相应的线程回处于改状态。改状态不占资源,当IO结束或者锁释放之后,改状态转化成RUNNABLE
WAITING:一个线程在执行某些方法之后,处于无限等待状态。比如object.wait(),thread.join(),LockSupport.park()。通过,Object.notify(),Object.notifyAll(),LockSupport.unpart(thread)唤醒线程。
TIMED_WAITING:改状态和waiting的差别在于处于该状态的线程,并非无限等待,而是由时间限制的等待,等超过时间就会被主动唤醒。
TERMINATED:已经执行结束的线程处于这个状态。
上下文切换是指存储和恢复cpu状态,使得程序能继续从断点处执行程序完成原先被中断的任务。

synchronized和volatile

指令重排列问题

原因:CPU数据计算流程:从内存读取数据–>CPU计算–>把计算结果写回内存。其中CPU与内存之间的IO速度,要比CPU的运算速度慢很多。为了解决这个问题,引入了高速缓存的概念,它一般位于内存与CPU之间,与CPU之间有较高的IO速度。因此就有了缓存命中率的问题。

int a = 0;a = a + 10;int b = 0;b = b + 5;a = a * 2;

以上代码,编译器认为在执行与a相关指令时,如第2行与第5行之间并没有其他与a有关的指令,因此,编译器的优化是在第2行执行完之后马上执行第5行,避免高速缓存的再次写入写出。也就可以发现,其实指令真正执行的顺序与所写的顺序并非一直。这就是指令的重排列问题。

java 内存结构及变量操作顺序

变量操作的顺序

  • lock(锁定)

作用于主内存的变量,它把一个变量标识为一个线程独占的状态;

  • unlock(解锁)

作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

  • read(读取)

作用于主内存的变量,它把一个变量的值从主内存传送到线程中的工作内存,以便随后的load动作使用;

  • load(载入)

作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;

  • use(使用)

作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎;

  • assign(赋值)

作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量;

  • store(存储)

作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作;

  • write(写入)

作用于主内存的变量,它把store操作从工作内存中得到的变量的值写入主内存的变量中。
其中1,8与多线程有关。

线程调度关系

调度关系图
java采用的抢占式调用策略

多线程中考虑的问题

1.内存一致性:对于java内存之间其实包含两层内存,一层是高速缓存,另一层是内存。而对于某个变量,它在主内存中的值,应该和在线程工作内存中的值是一致的。
2.内存可见性: 某个变量,如果一个线程对它进行修改,那么其他线程应该能立即看到它的变化。
3.执行的有序性:如果一个线程A依赖另一个线程B的执行结果,那么在线程B看来是串行执行的指令(其实可能经过了指令重排),在线程A看来,就是一个错误的执行顺序。
Java 多线程之 synchronized 和 volatile 的比较

volatile 的原理

对于多线程编程来说,每个线程是可以拥有共享内存中变量的一个拷贝。如果一个变量被 volatile 关键字修饰时,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝的,只是从共享变量中去读取数据。volatile只能保证内存的可见性,但是不能保证执行的原子性。

synchronized原理

我们说 synchronized 实际上是对变量进行加锁处理。那么不管是读也好,写也好都是基于对这个变量的加锁操作。如果一个变量被 synchronized 关键字修饰,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读就是将共享内存中的值刷新到本地内存,再从本地内存中读取数据。因为全过程中变量是加锁的,其他线程无法对这个变量进行读写操作。所以可以理解成对这个变量的任何操作具有原子性,即线程是安全的。正因为synchronized使得变量操作进行阻塞式操作,这就保证了只有一个线程对变量进行操作,也就使得变量的修改对内存是可见的,同时针对某一块代码块来说,只有一个线程对其进行执行,因此对其进行指令重排列不会影响执行的正确性。

综上所述,对于synchronized和volatile之间的区别,synchronized保证了原子性和内存可见性,并且在支持重排列,而volatile只保证了内存可见性,无法保证原子性,并且禁止了指令重排列。此外synchronized引起了上下文切换,而volatile没有,性能上synchronized开销更大。

ThreadLocal

ThreadLocal用于创建线程的本地变量,一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。

每个线程都会拥有他们自己的Thread变量,它们可以使用get()\set()方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal实例通常是希望它们同线程状态关联起来是private static属性。

守护线程和用户线程

用户线程:我们在java程序中创建的线程
守护线程:程序后台的线程

线程优先级

可以自己给每个线程设置优先级,但并不能保证高优先级的线程先执行,主要还是看操作系统中的调度算法。

线程之间的通信

object类中的notify,notifyAll,wait方法能作为线程通信时,可以作为竞争资源的锁的状态。
之所以将这些方法放在object中,而不是将它们放在Thread中,主要的原因是java提供的锁是对象级的锁,而不是线程级的锁。如果将这些方法放在Thread中,那么当Thread进行wait时,整个线程不清楚因为什么状态进行等待了,而要是设置在对象中,那可以明确的知道是因为哪个对象使得线程被阻塞。

Thread中yield和sleep方法

yield方法是指放弃资源竞争的意思,sleep是指线程愿意等待一段时间之后。这对于正在运行的线程来说是没有意义的,因此这些方法不会出现在具体的某个实例中,只属于Thread类,是静态的。

线程安全

安全等级

不可变

可以是基本类型的final;可以是final对象,但对象的行为不会对其状态产生任何影响,比如String的subString就是new一个String对象各种Number类型如BigInteger和BigDecimal等大数据类型都是不可变的,但是同为Number子类型的AtomicInteger和AtomicLong则并非不可变。原因与它里面状态对象是unsafe对象有关,所做的操作都是CAS操作,可以保证原子性。

绝对线程安全

线程运行不需要与外部进行交互,不需要线程同步操作。

相对线程安全

保证对象的基本操作是线程安全的,比如Vector,HashTable,synchronizedCollection包装集合等。

线程兼容

对象本身不是线程安全的,但是可以同步实现,比如ArrayList,HashMap等。

线程对立

不管调用端是否采用了同步的措施,都无法在并发中使用的代码。

实现方式

互斥同步

在多线程访问的时候,保证同一时间只有一条线程使用临界区(Critical Section),互斥量(Mutex),信号量(Semaphore)都是同步的一种手段。
java里最基本的互斥同步手段是synchronized,编译之后会形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象,还有一个锁的计数器,来记录加锁的次数,加锁几次就要同样解锁几次才能恢复到无锁状态。

其实在“Java与线程”里已经提到,java的线程是映射到操作系统的原生线程之上的,不管阻塞还是唤醒都需要操作系统的帮忙完成,都需要从用户态转换到核心态,这是很耗费时间的,是java语言中的一个重量级(Heavyweight)操作,虽然虚拟机本身会做一点优化的操作,比如通知操作系统阻塞之前会加一段自旋等待的过程,避免频繁切换到核心态。

ReentrantLock相比于synchronized的优势:

  • 等待可中断:在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待.

  • 公平锁:按照申请锁的顺序来一次获得锁称为公平锁.synchronized的是非公平锁,ReentrantLock可以通过构造函数实现公平锁.new RenentrantLock(boolean fair)

  • 锁绑定多个条件:通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.通过await(),signal();

非阻塞同步

互斥和同步最主要的问题就是阻塞和唤醒所带来的性能问题,所以这通常叫阻塞同步(悲观的并发策略)。随着硬件指令集的发展,我们有另外的选择:基于冲突检测的乐观并发策略,通俗讲就是先操作,如果没有其他线程争用共享的数据,操作就成功,如果有,则进行其他的补偿(最常见就是不断的重试),这种乐观的并发策略许多实现都不需要把线程挂起,这种同步操作被称为非阻塞同步。

这类的指令有:
1)测试并设置(test-and-set)
2)获取并增加
3)交换
4)比较并交换(CAS)
5)加载链接/条件储存(Load-Linked/Store-Conditional LL/SC)

后面两条是现代处理器新增的处理器指令,在JDK1.5之后,java中才可以使用CAS操作,就是传说中的sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法的包装提供,虚拟机对这些方法做了特殊的处理,及时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,可以认为是无条件的内联进去。

原来需要对i++进行同步,但现在有了这种CAS操作来保证原子性,比如用AtomicInteger。 但是CAS存在一个ABA的问题。可以通过AtomicStampedReference来解决(鸡肋)。

无同步

有一些代码天生就是线程安全的,不需要同步。其中有如下两类:

  • 可重入代码(Reentrant Code)
    纯代码,具有不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等特征,它的返回结果是可以预测的。
  • 线程本地存储(Thread Local Storage)
    把共享数据的可见范围限制在同一个线程之内,这样就无须同步也能保证线程之间不出现数据争用问题。可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。

线程池

假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制
优点
1、降低资源消耗;
2、提高响应速度;
3、提高线程的可管理性
深入分析java线程池的实现原理
jdk1·5开始引入的Executor框架把任务的执行和提交进行解耦,只需要定义好任务,交给线程就好,不需要关心任务是如何执行,由哪个线程进行管理。

其他问题

同步方法与同步代码块

同步代码块更好,同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

原创粉丝点击