二、多线程
-
线程的状态以及转换流程
-
状态
NEW
初始状态,线程被构建,但还没有调用start()方法RUNNABLE
运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行中”BLOCKED
阻塞状态,表示线程阻塞于锁WAITING
等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)TIME_WAITING
超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回TERMINATED
终止状态,表示当前线程已经执行完毕
-
转换流程
当一个线程被new出来,就进入初始化状态
当调用线程的start()方法,该线程就进入就绪状态,等待被cup调度执行
当线程获取cpu资源开始执行,就进入运行状态。
-
当一个线调用了"如下:"方法,就进入阻塞状态。直到线程重新进入就绪状态,才有机会转到运行状态
- 线程调用了wait(),当前线程释放对象锁,进入等待队列,依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到了自动唤醒
- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态
- 线程调用sleep(),主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,线程阻塞。
- 程序调用了线程的suspend方法()将线程挂起,但该方法容易导致死锁,所以尽量不使用。(调用resume(恢复))
线程执行完必或者因异常退出了run()方法,则线程结束生命周期,呈终止状态
-
补充
yield()可以让当前正在执行的线程暂停,但不会阻塞线程,只是让该线程转入就绪状态。yield()让线程暂停一下,让线程调度器重新调度一次,完全有可能的是,调用了yield()方法后,线程调度器又将其调度出来重新执行
wait()释放锁对象 属于Object类 sleep()不释放锁对象 属于Thread类
-
-
如何创建一个线程
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程 可有返回值和抛出异常
- 使用线程池例如用Executor框架
-
线程同步的方式
-
使用synchronized实现同步
普通成员方法,锁是当前对象 静态同步方法,锁是当前类的Class对象 同步方法块,锁是Synchronized括号里配置的对象
- 同步代码块
synchronized(obj){ //obj共享的资源 //此处的代码就是同步代码块 }
- 声明同步方法
//在方法名前+synchrnize
- 同步代码块
-
同步锁Lock
private final ReentrantLock lock = new ReentrantLock(); lock.lock();//上锁,在try前 lock.unlock();//释放锁,在finally中
-
CountDownLatch
允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果 你想等待N个点完成就传入N。当每一个线程调用countDown(),计数器的值就会减1,CountDownLatch的await()方法会阻塞当前线程。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
-
使用线程变量实现线程同步
线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构,被附带在线程上,也就是每一个线程可以根据ThreadLocal对象查询到绑定在自己线程上的一个值,线程上的值是相互独立的,从而保证数据的读取安全性,实现同步。(其实破坏了线程的共享,每个线程上的不是同一份数据)
-
使用阻塞队列工具实现线程同步(BlockQueue)
put(E e),尝试放入元素,若队列已满,则阻塞该线程 take(),尝试取出元素,若队列已空,则阻塞该线程 比如:想读取一个数据,但它没修改好未put进来,就把读的线程阻塞;数据刷新进来后,就可以读取了。
-
使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步.
//AtomicInteger AtomicBoolean AtomicLong class Bank { private AtomicInteger account = new AtomicInteger(100); public AtomicInteger getAccount() { return account; } public void save(int money) { account.addAndGet(money); } }
-
注意:volatile不能保证线程安全,对单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性
-
线程间通信的方式
-
传统的线程通信
借助Object类的wait()、notify()、notifyAll(),但必须由同步监视器对象来调用
-
使用Condition控制线程通信
当使用Lock来保证线程同步时,可以借助Condition来保持协调:await()、signal()、signalAll()
-
使用阻塞队列(BlockQueue)
put(E e) 尝试放入元素,若队列已满,则阻塞该线程 take() 尝试取出元素,若队列已空,则阻塞该线程
-
管道的输入/输出
对于Piped类型的流,必须要进行绑定,也就是调用connect()方法,否则会抛异常
-
Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止后才从thread.join()返回
ThreadLocal的使用
-
-
volatile关键字
如果一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么它就具有了两层含义:
-
可见性,保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对于其他线程来说是立即可见的
如何保证:
保证可见性的两条实现原则(java代码转汇编代码时,volatile修饰的变量在读写操作时出现Lock前缀指令,该指令在多核处理器上下会引发两件事:
Lock前缀指令会引起处理器缓存回写到内存
一个处理器的缓存回写到内存会导致其他处理器的缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存。在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探总线上传播的数据来检查自己缓存的值是不是过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效,当需要对数据进行操作时,就会重新到系统内存中把数据读到处理器缓存中。
禁止进行指令重排序,一定程度上保证了有序性(编译器重排序和处理器重排序(在指令序列中插入内存屏障))
volatile变量自身具有以下的特征:
可见性
原子性,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
Java线程之间的通信
Java线程之间的通信由Java内存模型控制,决定一个线程对共享变量的写入何时对另一个线程可见
每个线程运行过程中都有自己的工作内存,会将变量值拷贝一份放在自己的工作内存当中。被volatile修饰的变量值修改后,会强制的立即写入主存中;当一个线程修改变量时,会将其它线程的工作内存中的缓存变量的缓存行无效,当线程的工作内存中变量的缓存行无效,该线程读取变量的值时会直接去主存里去读取。
-
-
synchronized关键字
-
synchronized简介
synchronized实现同步的基础:Java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。
Synchronzied实现同步的表现形式分为:代码块同步 和 方法同步。
-
synchronized原理
JVM基于进入和退出Monitor对象来实现 代码块同步和方法同步,两者实现细节不同。
-
同步代码块
在编译后通过将monitorenter指令插入到同步代码块的开始处,将monitorexit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,就处于锁定状态。线程执行monitorenter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁。
-
方法同步
synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。
-
两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
-
-
lock和synchronize区别
synchronized隐式的获取释放锁,lock显示的获取释放锁
synchronized是在JVM层面上实现的,内置的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,发生异常时候,不会主动释放占有的锁,必须手动释放锁,将unLock()放到finally{}中释放锁,避免死锁的发生。
-
Lock接口来实现锁的功能,拥有锁释放获取的可操作性、可中断的获取锁、超时获取锁等同步特性
-
尝试非阻塞的获取锁(boolean tryLock())
描述:当前线程尝试获取锁,如果这一刻锁没有被其他线程获取到,则成功获取并持有锁
-
能被中断的获取锁(void lockInterruptibly())
描述:与synchronize不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁会被释放
-
超时获取锁(boolean tryLock(long time,TimeUnit unit) )
描述:在指定的截止时间之前释放锁,如果截止时间到了仍无法获取到锁,则返回
-
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
-
synchronized是非公平锁,而lock可以是非公平锁也可以是公平锁.用布尔值来选择类型。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁(并不是绝对的,大体上是这种顺序),这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
public ReentrantLock(boolean fair) { sync = fair new FairSync() : new NonfairSync(); }
-
Java静态属性与静态方法可否被继承的问题
结论:Java中静态属性和静态方法能够被继承,不可以重写静态方法
静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成调用,不须要继承机制。若是子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为"隐藏"。若是你想要调用父类的静态方法和属性,直接经过父类名.方法或变量名来完成调用。因此,可否继承一说,子类是有继承静态方法和属性,可是跟实例方法和属性不太同样,存在"隐藏"的这种状况。
静态属性、静态方法和非静态的属性均可以被继承和隐藏而不能被重写,所以不能实现多态,不能实现父类的引用能够指向不一样子类的对象。而非静态方法能够被继承和重写,所以能够实现多态。
参考书籍:
《Java并发编程的艺术》
《疯狂Java讲义》