Java 实现多线程的两种方式 :
1> 继承 Thread 类
2> 实现 Runnable 接口 : 这种方式可不影响类继承其它类,另外还可以 创建一个
Runnable 实例做为多个 Thread 的执行用以实现多线程数据共享
临界区 : 一个用以访问共享资源的代码块,这个代码块同一时间内只允许一个线程执行
当一个线程试图访问一个临界区时,它将使用一个同步机制来查看是不是已经有其他线程进入临界区。如果没有其他线程进入临界区,它就可以进入临界区;如果已经有线程进入临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM 会选择其中一个,其余的将继续等待。
synchronized关键字声明的方法都是临界区,保证并发程序中对共享数据的正确访问 (就算是方法不一样但存在共享数据) 也会保证并发的正确性
内置锁 : 每个 java对象 都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去
对象锁和类锁:java对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助理解锁定实例方法和静态方法的区别的
锁提供两种主要特性 : 互斥和可见性,互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的
synchronized 关键字
synchronized 是一个同步锁,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码
1> 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
2> 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
3> 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
4> 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象
注 : 当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题
修饰方法注意几点
1> synchronized关键字不能继承 : 可以使用 synchronized 来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步
2> 在定义接口方法时不能使用synchronized关键字
3> 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步
例如如下代码 :
public class
SynchronizedTest
implements
Runnable {
public void
method1(Object obj) {
// 访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞
synchronized
(
this
) {
// 锁定当前对象
}
}
public void
method2(Object obj) {
synchronized
(obj) {
// obj 锁定的对象
}
}
// 当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁
// 零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码
private byte
[]
lock
=
new byte
[
0
];
public void
method3() {
synchronized
(
lock
) {
}
}
public synchronized void
method4() {
// 锁定整个函数
}
public synchronized static void
method5() {
// 修饰静态方法
}
@Override
public synchronized void
run() {
method5
();
}
public void
method6() {
synchronized
(SynchronizedTest.
class
) {
// 锁定类,给类加锁,类的所有对象用的同一把锁
}
}
}
总结
1> 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁
2> 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码
3> 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
volatile 关键字
关键字 volatile 被称作轻量级的 synchronized,与synchronized相比,volatile编码相对简单且运行开销较少,操作不会像锁一样容易造成阻塞,但能够正确合理的应用好 volatile 并不是那么的容易,因为它比使用锁更容易出错
它所修饰的变量不保留拷贝,直接访问主内存中的
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的 memory 中的值可能与另外一个线程 memory 中的值,或者 main memory 中的值不一致的情况。 一个变量声明为 volatile,就意味着这个变量是随时会被其它线程修改的,因此不能将它 cache 在线程 memory 中
volatile 实现原理
如果对声明 volatile 变量进行写操作,JVM就会向处理器发送一条 lock前缀的指令,该 lock指令会使这个变量所在缓存行的数据回写到系统内存,根据缓存一致性协议,每个处理器都会通过嗅探在总线上传输的数据来检查自己缓存的值是否已过期,当处理器发现自己的缓存行对应的地址被修改,就会将当前处理器的缓存行设置成无效状态,在下次访问相同内存地址时,强制执行缓存行填充
正确使用 volatile 场景
volatile 主要用来解决多线程环境中内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,就无法解决线程安全问题,如 :
1> 不适合使用volatile的场景 (非原子性操作)
(1) 反例
private static volatile int
nextSerialNum
=
0
;
public static long
generateSerialNum() {
return
nextSerialNum
++;
}
这个方法的目的是要确保每次调用都返回不同的自增值,然而结果并不理想,问题在于增量操作符 (++) 不是原子操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程与第一个线程就会读取到同一个值
注 : 可以使用JDK1.5 java.util.concurrent.atomic中提供的原子包装类型来保证原子性操作
(2) 正例
private static
AtomicInteger
nextSerialNum
=
new
AtomicInteger(
0
);
public static long
generateSerialNum() {
return
nextSerialNum
.getAndIncrement();
}
2> 适合使用volatile的场景
在日常工作当中 volatile 大多被在状态标志的场景当中,如 : 要通过一个线程来终止另外一个线程的场景
(1) 反例
private static boolean
stopThread
;
public static void
main(String[] args)
throws
InterruptedException {
Thread th =
new
Thread(
new
Runnable() {
public void
run() {
int
i =
0
;
while
(!
stopThread
) {
i++;
}
}
});
th.start();
TimeUnit.SECONDS.sleep(
2
);
stopThread
=
true
;
}
运行后发现该程序根本无法终止循环,原因是,java语言规范并不保证一个线程写入的值对另外一个线程是可见的,所以即使主线程main函数修改了共享变量stopThread状态,但是对 th线程并不可见,最终导致循环无法终止
(2) 正例
private static volatile boolean
stopThread
;
public static void
main(String[] args)
throws
InterruptedException {
Thread th =
new
Thread(
new
Runnable() {
public void
run() {
int
i =
0
;
while
(!
stopThread
) {
i++;
}
}
});
th.start();
TimeUnit.
SECONDS
.sleep(
2
);
stopThread
=
true
;
}
使用关键字volatile修饰共享变量stopThread,根据volatile的可见性原则可以保证主线程main函数修改了共享变量stopThread状态后对线程th来说是立即可见的,所以在两秒内线程th 将停止循环
区别
1> volatile 是变量修饰符,而synchronized则作用于一段代码或方法
2> volatile 只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源
Object 类
在 Java 中,是没有类似于 PV操作、进程互斥等相关的方法的。在 Java 中的 Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现 Java 中简单的同步、互斥操作,如果需要在线程间相互唤醒的话就需要借助 Object.wait(), Object.nofity()
Object.wait(),与 Object.notify() 必须要与 synchronized(Obj) 一起使用,也就是 wait 与 notify 是针对已经获取 Object锁进行操作,从语法角度来说就是 Object.wait(), Object.notify 必须在 synchronized(Object){...}语句 块内。从功能上来说 wait 就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的 notify() 唤醒该线程,才能继续获取对象锁,并继续执行。相应的 notify() 就是对对象锁的唤醒操作。但有一点需要注意的是 notify()调用后,并不是马上就释放对象锁的,而是在相应的 synchronized(){}语句块执行结束,自动释放锁后,JVM 会在 wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供在线程间同步、唤醒的操作
public final native void notifyAll() : 解除所有那些在该对象上调用wait方法的线程的阻塞状态,该方法只能在同步方法或同步块内部调用,如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常
public final native void notify() : 随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,该方法只能在同步方法或同步块内部调用,如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常
public final native void wait(long timeout)
public final void wait(long timeout, int nanos)
public final void wait() : 导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒,带参数的方法指定延迟时间,该方法只能在同步方法中调用,如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常
Thread.sleep() 与 Object.wait() 二者都可以暂停当前线程,释放 CPU控制权,主要的区别在于 Object.wait() 在释放 CPU同时,释放了对象锁的控制
实例 : 建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC
public class
MyThreadPrinter
implements
Runnable {
private
String
name
;
private
Object
prev
;
private
Object
self
;
private
MyThreadPrinter(String name, Object prev, Object self) {
this
.
name
= name;
this
.
prev
= prev;
this
.
self
= self;
}
@Override
public void
run() {
int
count =
10
;
while
(count >
0
) {
synchronized
(
prev
) {
synchronized
(
self
) {
System.
out
.print(
name
);
count--;
try
{
Thread.
sleep
(
1
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
self
.notify();
}
try
{
prev
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void
main(String[] args)
throws
Exception {
Object a =
new
Object();
Object b =
new
Object();
Object c =
new
Object();
new
Thread(
new
MyThreadPrinter(
"A"
, c, a)).start();
Thread.
sleep
(
10
);
new
Thread(
new
MyThreadPrinter(
"B"
, a, b)).start();
Thread.
sleep
(
10
);
new
Thread(
new
MyThreadPrinter(
"C"
, b, c)).start();
Thread.
sleep
(
10
);
}
}
Thread 类
Thread 类有一些信息属性,其用于标识线程、状态、优先级
1> ID : 线程的唯一标识符,不可修改
2> Name : 线程名称,默认名称的格式为 Thread-XX,可修改
3> Priority : 线程对象的优先级,从 1 到 10,其中 1是最低优先级 10 是最高,不推荐修改优先级,若修改不在指定范围则抛出异常,例如如下一些常量
public final static int
MIN_PRIORITY
=
1
;
public final static int
NORM_PRIORITY
=
5
;
public final static int
MAX_PRIORITY
=
10
;
4> Status : 线程状态,在 Java 中有6种状态 new、runnable、blocked、waiting、time waiting、terminated
currentThread() : 静态方法用于获取当前执行线程对象
public static native
Thread currentThread();
join方法 : 当前线程等待目标线程结束后在继续运行,或者在指定时间后继续运行
public final synchronized void
join(
long
millis)
public final void
join()
throws
InterruptedException
sleep方法 : 用于阻塞当前线程,在指定毫秒后继续运行
public static native void
sleep(
long
millis)
throws
InterruptedException;
public static void
sleep(
long
millis,
int
nanos)
throws
InterruptedException
线程中断
interrupt方法 : 中断线程,如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException
public void
interrupt()
interrupt() 方法源码如下,线程的 blocker字段(也就是interrupt status)默认是null,调用interrupt()方法时,并没有进入if语句,所以没调用真正执行中断的代码 b.interrupt(),若是有阻塞线程的方法此时 blocker 不为 null 这种情况下将会抛出异常
public void
interrupt() {
if
(
this
!= Thread.
currentThread
())
checkAccess();
synchronized
(
blockerLock
) {
Interruptible b =
blocker
;
if
(b !=
null
) {
interrupt0();
// Just to set the interrupt flag
b.interrupt(
this
);
return
;
}
}
interrupt0();
}
interrupt status (中断状态),在Java源码中代表中断状态的局部变量是 blocker
private volatile Interruptible blocker;
对 Interruptible类不需要深入分析,对于 blocker变量有以下几个操作 :
1> 默认blocker=null;
2> 调用方法 interrupt0();将会导致该线程的中断状态将被设置
3> 再次调用 interrupt0();将会导致其中断状态将被清除
注 : 是 interrupt0() 方法是谁,不是 interrupt() 方法
interrupted()静态方法 : 用于判断当前线程是否阻断,线程的中断状态 由该方法清除,同时还可以得到上一次中断标志值。线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来
public static boolean
interrupted()
isInterrupted()方法 : 测试线程是否已经中断,线程的中断状态不受该方法的影响。线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来
public boolean
isInterrupted()
interrupted() 和 isInterrupted() 两个方法的相同点和不同点 :
相同点 : 都是判断线程的interrupt status是否被设置,若被设置返回true,否则返回false
区别点 : 1> 前者是static方法,调用者是current thread,而后者是普通方法,调用者是this current
2> 它们其实都调用了Java中的一个native方法 isInterrupted(boolean ClearInterrupted); 不同的是前者传入了参数true,后者传入了false。意义就是:前者将清除线程的interrupt state,调用后者线程的interrupt state不受影响
例如如下代码 :
public class
FileSearch
implements
Runnable {
private
String
initPath
;
private
String
fileName
;
public
FileSearch(String initPath, String fileName) {
this
.
initPath
= initPath;
this
.
fileName
= fileName;
}
@Override
public void
run() {
File file =
new
File(
initPath
);
if
(file.isDirectory()) {
try
{
directoryProcess(file);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
private void
directoryProcess(File file)
throws
InterruptedException {
File list[] = file.listFiles();
if
(list !=
null
) {
for
(
int
i =
0
; i < list.
length
; i++) {
if
(list[i].isDirectory()) {
directoryProcess(list[i]);
}
else
{
fileProcess(list[i]);
}
}
}
if
(Thread.
interrupted
()) {
// 当调用 interrupt 方法时中断状态被设置,此时返回true,且中断状态将会被清理
throw new
InterruptedException();
}
}
private void
fileProcess(File file)
throws
InterruptedException {
if
(file.getName().equals(
fileName
)) {
System.
out
.printf(
"%s : %s
\n
"
, Thread.
currentThread
().getName(), file.getAbsolutePath());
}
if
(Thread.
interrupted
()) {
// 当调用 interrupt 方法时中断状态被设置,此时返回true,且中断状态将会被清理
throw new
InterruptedException();
}
}
public static void
main(String[] args) {
FileSearch fileSearch =
new
FileSearch(
"/Users/mew/Desktop"
,
"a.txt"
);
Thread thread =
new
Thread(fileSearch);
thread.start();
try
{
TimeUnit.
SECONDS
.sleep(
10
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
在Java中有两类线程 : 用户线程 (User Thread)、守护线程 (Daemon Thread)
守护线程 :
优先级低,在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个典型的守护线程,并且这种线程并不属于程序中不可或缺的部分
,通常其是无限循环以等待服务请求或者执行线程的任务,不能做重要的工作因为无法确定什么时候守护线程可以获取CPU时钟并且没有其它线程运行的时候守护线程随时可能结束。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止
用户线程 : 和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开,如果用户线程已经全部退出运行了,只剩下守护线程存在,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了
setDaemon方法 : 设置线程为守卫线程,该方法只能在 start() 方法前调用,一旦线程开始运行,将不能再修改守护状态
public final void
setDaemon(
boolean
on)
public final boolean
isDaemon()
线程转换为守护线程需要注意的地方 :
1> thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常 (不能把正在运行的常规线程设置为守护线程)
2> 在Daemon线程中产生的新线程也是Daemon的
3> 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断
不可控异常
setUncaughtExceptionHandler 方法 : 设置未知性异常捕获
public void
setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
public static
UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
public
UncaughtExceptionHandler getUncaughtExceptionHandler()
例如如下实例 :
public class
ExceptionHandler
implements
Thread.UncaughtExceptionHandler {
@Override
public void
uncaughtException(Thread t, Throwable e) {
System.
out
.printf(
"Exception: %s: %s
\n
"
, e.getClass().getName(), e.getMessage());
e.printStackTrace(System.
out
);
}
}
public class
Task
implements
Runnable {
@Override
public void
run() {
int
numero = Integer.
parseInt
(
"TTT"
);
}
public static void
main(String[] args) {
Task task =
new
Task();
Thread thread =
new
Thread(task);
thread.setUncaughtExceptionHandler(
new
ExceptionHandler());
thread.start();
}
}
使用工厂类创建线程,只需要继承 java.util.concurrent.ThreadFactory 类
public class
MyThreadFactory
implements
ThreadFactory {
private int
counter
;
private
String
name
;
public
MyThreadFactory(String name) {
this
.
counter
=
0
;
this
.
name
= name;
}
@Override
public
Thread newThread(Runnable r) {
Thread thread =
new
Thread(r,
name
+
"-Thread_"
+
counter
);
counter
++;
return
thread;
}
public static void
main(String[] args) {
MyThreadFactory myThreadFactory =
new
MyThreadFactory(
"MyThreadFactory"
);
Thread thread;
for
(
int
i =
0
; i <
10
; i++) {
thread = myThreadFactory.newThread(
new
Runnable() {
@Override
public void
run() {
}
});
thread.start();
}
}
}
TimeUnit 枚举
TimeUnit 枚举,可以使用其中的枚举数据来设置不同的时间类别的时间
TimeUnit.
SECONDS
.sleep(
4
);
TimeUnit.
MICROSECONDS
.timedJoin(thread1,
10000
);
ThreadLocal 类
当使用 ThreadLocal 维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量
ThreadGroup 类
ThreadGroup 代表一个线程组
,线程组包含线程对象,其也可以包含
其它线程组对象或者加入到另外一个线程组中,
它是一个树形结构。线程组可以
方便统一管理,线程组可以进行复制,快速定位到一个线程,统一进行异常设置等
注 :
main方法执行后,将自动创建 system线程组合 main线程组,main方法所在线程存放在main线程组中
ThreadGroup threadGroup =
new
ThreadGroup(
"Searcher"
);
Thread thread =
new
Thread(threadGroup, searchTask);
Thread[] threads =
new
Thread[threadGroup.activeCount()];
// activeCount() : 获取激活数组的数量
threadGroup.enumerate(threads);
// 拷贝激活的线程组
for
(
int
i =
0
; i < threadGroup.activeCount(); i++) {
System.
out
.printf(
"Thread %s: %s
\n
"
, threads[i].getName(), threads[i].getState());
}
threadGroup.interrupt();
// 阻断线程组中所有的线程
捕获线程组中未知性异常
public class
MyThreadGroup
extends
ThreadGroup {
public
MyThreadGroup(String name) {
super
(name);
}
@Override
public void
uncaughtException(Thread t, Throwable e) {
e.printStackTrace(System.
out
);
interrupt();
}
public static void
main(String[] args) {
MyThreadGroup myThreadGroup =
new
MyThreadGroup(
"MyThreadGroup"
);
for
(
int
i =
0
; i <
2
; i++) {
Thread thread =
new
Thread(myThreadGroup,
new
Runnable() {
@Override
public void
run() {
}
});
thread.start();
}
}
}
ReentrantLock 类
ReentrantLock一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大
注 : ReentrantLock持有的是 对象监视器
和 synchronized
对象监视器不同,
ReentrantLock持有的锁是需要手动去unlock()的
public void lock() : 请求锁,如果锁未被另外一个线程持有立即返回并设置 锁持有数 为1,如果当前线程已经持有锁此时 锁持有数 加1且方法立即返回,如果锁被另外一个线程持有此时当前线程被阻塞线程休眠直到可以获取线程锁
public boolean tryLock() : 只有在不被另外一个线程持有时请求锁,如果没有被另外一个线程持有锁时请求锁则立即返回 true 返回值并设置 锁持有数为1,甚至当锁被设置使用一个公平排序策略如果锁可以被获取无论其它线程是否正在等待锁 一个 tryLock() 调用将会立即获取,这种运输行为在特定场景非常有用即使打破公平性,如果你想当前锁遵循公平设置此时使用 tryLock(0, TimeUnit.SECONDS) 差不多公平,如果当前线程已经持有这个锁此时 锁持有数加一 且返回 true,如果锁被另外一个线程持有此时这个方法将立即返回 false 值
注 : lock 和 tryLock 都用于获取锁,lock 获取不到锁线程会阻塞,tryLock 不管成功有否都将返回 boolean 并继续执行
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException : 如果没有被其它线程在给定时间内持有且当前线程没有阻断线程则获取锁。如果锁没有被其它线程持有则获取锁且立即返回 true 并设置 锁持有数 为1,如果锁已经被设置使用一个公平顺序策略此时 如果任何其它线程正在等待锁 此时一个可获取的锁将不会获取 这点和 tryLock() 方法是相对的,如果你需要一个限时 tryLock 公平锁此时可以结合 限时和非限时
if (lock.tryLock() || lock.tryLock(timeout, unit)) {
...
}
如果当前线程已经持有这个锁此时 锁持有数 加1且返回 true,如果锁已经被另外一个线程持有此时当前线程停止运行直到树形事件发生 :
1> 锁已经被当前线程获取 或者 一些其它线程
2> 一些其它的线程阻碍当前线程
3> 特殊等待时间流逝
如果锁被获取此时返回 true 而且 锁持有数 为1
如果当前线程 :
1> 从阻塞状态设置成进入这个方法
2> 在获取锁时线程阻塞
此时抛出 InterruptedException 当前线程阻塞状态被清理。如果超过特殊等待时间此时返回false,如果时间小于或等于 0,方法不会等待
在此实现中,由于该方法是一个显式的中断点,因此,首选项是响应对锁的正常或重入获取的中断,并报告等待时间的间隔
public void unlock() : 尝试释放锁,如果当前线程是锁持有者此时 锁持有数 减1,如果 锁持有数 为0则锁会被释放,如果当前线程不是锁持有者则抛出异常。必须使用此方法来释放其持有的锁,否则永久等待从而导致死锁,若错在 try-cache 块则可在 finally 中放入unlock() 方法
synchronized锁配合的线程等待 (Object.wait) 与线程通知(Object.notify), java.util.concurrent.locks.ReentrantLock 锁也提供与此功能相应的类java.util.concurrent.locks.Condition,Condition与重入锁是通过lock.newCondition()方法产生一个与当前重入锁绑定的Condtion实例
public Condition newCondition() : 创建 Condition 实例,在使用条件的时候必须获取这个条件绑定的锁所以带条件的代码必须在调用 Lock 对象的 lock() 方法和 unlock() 方法之间
lock
.lock();
try
{
while
(
buffer
.size() ==
maxSize
) {
condition1.await();
}
buffer
.offer(line);
condition2.signalAll();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
finally
{
lock
.unlock();
}
Condition 接口
1> 一个Lock里面可以创建多个Condition实例,实现多路通知
2> notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是 ReentrantLock 结合 Condition 可以实现有选择性地通知
void await() : 使当前线程加入 await() 等待队列中,并释放当锁,当其他线程调用signal()会重新请求锁,与Object.wait()类似
void awaitUninterruptibly() : 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的 signal() 或 signalALL() 方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态
long awaitNanos(long nanosTimeout) throws InterruptedException : 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException,nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数
boolean await(long time, TimeUnit unit) throws InterruptedException : 与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true
boolean awaitUntil(Date deadline) throws InterruptedException : 适用条件与行为与 awaitNanos(long nanosTimeout) 完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻
void signal() : 唤醒一个在 await() 等待队列中的线程,在线程唤醒后就会尝试重新获得与之绑定的重入锁一旦获取成功将继续执行,与 Object.notify() 相似
void signalAll() : 唤醒 await() 等待队列中所有的线程,与 object.notifyAll() 相似
ReentrantReadWriteLock 类
读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持,写入锁是独占的
互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作;读写锁允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程 (write线程) 可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取
与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用 (即在同一时间试图对该数据执行读取或写入操作的线程数)
注 : 读操作锁允许多个线程同时访问,但写操作锁只允许一个线程进行,在一个线程执行写操作时,其他线程不能够执行读操作
读写锁适用于读多写少的情况
private double
price1
;
private double
price2
;
private
ReadWriteLock
readWriteLock
;
public
PriceInfo() {
price1
=
1.0
;
price2
=
2.0
;
readWriteLock
=
new
ReentrantReadWriteLock();
}
public double
getPrice1() {
readWriteLock
.readLock().lock();
double
value =
price1
;
readWriteLock
.readLock().unlock();
return
value;
}
public double
getPrice2() {
readWriteLock
.readLock().lock();
double
value =
price2
;
readWriteLock
.readLock().unlock();
return
value;
}
public void
setPrices(
double
price1,
double
price2) {
readWriteLock
.writeLock().lock();
this
.
price1
= price1;
this
.
price2
= price2;
readWriteLock
.writeLock().unlock();
}
线程同步辅助类
信号量(Semaphore) : 是一种计数器,用来保护一个或者多个共享资源的访问
CountDownLatch : 在完成一族正在其他线程中执行的操作之前,它允许线程一直等待
CyclicBarrier : 允许多个线程在某个集合点进行相互等待
Phaser : 把并发任务分成多个阶段运行,在开始下一阶段之前,当前阶段中的所有线程都必须执行完成
Exchanger : 提供两个线程之间的数据交换点
信号变量
为了提高接口的响应速度,可以使用 ThreadPoolExecutor + Runnable 或者 ThreadPoolExecutor 并发调用技术来并行执行Task。但是ThreadPoolExecutor有个特点,就是当core线程不足以应付请求的时候,会将task加入到队列中。一旦使用队列,那么就可能出现队列爆掉或者队列导致的内存溢出问题。为了尽快提供接口响应速度,但是又不想使用队列特性的话。可以使用信号量来做到
Java 语言提供信号变量机制,信号量是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个共享资源,它必须先获得信号量。如果信号量的内部计数器大于0,信号量将减 1,然后允许访问这个共享资源。计数器大于0意味着有可使用资源,此时线程将被允许使用其中一个资源
否则,如果信号量的计数器等于0,信号量会把线程置入休眠直至计数器大于0。计数器等于0的时候意味着所有的共享资源已经被其它线程使用,所以需要访问这个共享资源的线程必须等待
当线程使用完某个共享资源时,信号量必须被释放,以便其他线程能够访问共享资源。释放操作将使信号变量的内部计数器增加 1
Semaphore信号量管理着一组许可,在执行操作时需要首先获得许可,并在使用后释放许可。如果已经没有许可了, acquire方法将一直阻塞,直到有许可。Semaphore可以用来实现有界阻塞容器
public Semaphore(int permits) : 创建一个给定数量许可并设置不公平模式
public Semaphore(int permits, boolean fair) : 第二参数为是否设置成公平模式
public void acquire() throws InterruptedException : 从信号量请求一个许可,一直阻塞线程直到可以获得一个许可或者线程阻断。如果可以立即获得一个许可,可获得许可数量将会减1。如果没有许可可以获取此时此时当前线程进程进入休眠,直到如下两件事情发生 :
1> 另外某个线程调用 release() 方法且当前线程时获得这个许可
2> 另外某个线程 Thread.interrupt 当前线程
如果当前线程 :
1> 处于阻塞状态设置方法的进入状态
2> 正在等一个许可 Thread.interrupt 此时 InterruptedException 将被抛出且当前线程阻塞状态将被清理
public void acquireUninterruptibly() : 就是 acquire() 方法,当前信号量的内部计数器变成 0的时候,信号量将阻塞线程直到其被释放。线程在被阻塞的这段时间中,可能会被中断,从而导致 acquire()方法抛出 InterruptedException,而当前方法不会抛出任何异常
public boolean tryAcquire() : 尝试获取信号量,如果能获得就返回 true,否则为 false,不会线程
通常把一个非负整数称为Semaphore,表示为 S
S实现的同步机制表示为 PV原语操作
P(S) : 若S=0,线程进入等待队列;否则,-S
V(S) : ++S,唤醒处于等待中的线程
注 : P是荷兰语的Passeren,相当于英文的pass,V是荷兰语的Verhoog,相当于英文中的incremnet
有如下例子 :
public class
BoundedHashSet<
T
> {
private final
Set<
T
>
tempSet
;
private final
Semaphore
sem
;
public
BoundedHashSet(
int
size) {
this
.
tempSet
= Collections.
synchronizedSet
(
new
HashSet<
T
>());
sem
=
new
Semaphore(size);
}
public boolean
add(
T
o)
throws
Exception {
sem
.acquire();
boolean
isAdd =
false
;
try
{
isAdd =
tempSet
.add(o);
return
isAdd;
}
finally
{
if
(isAdd) {
System.
out
.println(
"["
+ o +
"]数据添加成功"
+ showTime());
}
else
{
System.
out
.println(
"["
+ o +
"]数据添加失败"
+ showTime());
sem
.release();
}
}
}
public boolean
remove(Object o) {
boolean
isRemoved =
tempSet
.remove(o);
if
(isRemoved) {
System.
out
.println(
"["
+ o +
"]数据删除成功"
+ showTime());
sem
.release();
}
else
{
System.
out
.println(
"["
+ o +
"]数据删除失败"
+ showTime());
}
return
isRemoved;
}
public
Set<
T
> getAllData() {
return
tempSet
;
}
private
String showTime() {
return new
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss"
).format(
new
Date());
}
public static void
main(String[] args)
throws
Exception {
BoundedHashSet<String> set =
new
BoundedHashSet<String>(
2
);
set.add(
"1"
);
set.add(
"2"
);
set.remove(
"2"
);
set.add(
"3"
);
System.
out
.println(JSON.
toJSONString
(set.getAllData()));
set.add(
"4"
);
set.remove(
"3"
);
System.
out
.println(JSON.
toJSONString
(set.getAllData()));
}
}
例子实现了有界阻塞的HashSet,只允许这个HashSet存放两个元素,如果想存第三个元素,必须等到有人把HashSet中的元素remove掉。每次add之前先申请一个许可,如果能申请到,则正常添加元素。申请不到,则acquire()方法会一直阻塞。remove操作里面,则有一个释放许可的操作。因此第一个 System.out.println 之后的操作都无法继续
在 ThreadPoolExecutor 中,如果不想用到队列,就必须保证线程池中始终只有core线程在工作。那么当请求太多,core线程处理不过来的时候,用信号量进行阻塞,保证只有当core线程的某些线程执行完后,阻塞才解开
CountDownLatch 类
在完成一组正在其它线程中执行的操作之前,它允许线程一直等待。这个类使用一个整数进行初始化,这个整数就是线程要等待完成的操作的数目。当一个线程要等待某些操作先执行完时,需要调用 await() 方法,这个方法让线程进入休眠直到等待的所有操作都完成。当某一个操作完成后,它将调用 countDown() 方法将 CountDownLatch 类的内部计数器减1。当计数器变成 0 的时候,CountDownLatch 类将唤醒所有调用 await() 方法进入休眠的线程
1> CountDownLatch 机制不是用来保护共享资源或者临界区,而是用来同步执行多个任务或者多个线程
2> 只准进入一次,一旦 CountDownLatch 内部计数为0,再调用这个方法将不起作用,除非创建新的 CountDownLatch
public CountDownLatch(int count) : 使用给定数量进行初始化,指定数量的 countDown 必须调用线程才能运行
public void await() throws InterruptedException : 当前线程一直等待直到数量减到0,或者是 Thread.interrupt。如果当前执行数为0则该方法立即返回。如果当前数量大于0则当前线程休眠直到如下事情发生 :
1> 如果由于调用 countDown方法 数量抵达0
2> 其它线程调用 Thread.interrupt
如果当前线程 :
1> 处于阻塞状态设置方法的进入状态
2> 正在等一个许可 Thread.interrupt 此时 InterruptedException 将被抛出且当前线程阻塞状态将被清理
public boolean await(long timeout, TimeUnit unit) : 和 await() 一样但是多了超期时间
public void countDown() : 减少 数量,如果数量到0释放所有等待线程,如果当前数量大于0则减少,如果新数量是0则所有等待线程被唤醒,如果当前为0什么也不发生
CyclicBarrier 类
允许两个或者多个线程在某个点上进行同步,CyclicBarrier类使用一个整型数进行初始化,这个数是需要在某个点上进行同步线程数。当一个线程到达指定点后,它将调用 await() 方法等待其它线程。当线程调用 await()方法后,CyclicBarrier类将阻塞这个线程并使之休眠直到所有其它线程到达。当最后一个线程调用 CyclicBarrier类的 await()方法时,CyclicBarrier对象将唤醒所有在等待的线程,然后这些线程将继续执行
此外 CyclicBarrier类可传入另外一个 Runnable对象作为初始化参数,当所有的线程都到达集合点后,CyclicBarrier类这个Runnable对象将作为线程执行
public CyclicBarrier(int parties) : CyclicBarrier构造函数,指定内部计数器数量,线程会阻塞在 await() 处,在到达指定数量 await() 处时,所有的阻塞线程将继续执行
public CyclicBarrier(int parties, Runnable barrierAction) : 其它地方和CyclicBarrier(int parties)一样,唯一不同的是所有的阻塞线程将继续执行且 barrierAction 将会加入到运行中
public int await() throws InterruptedException, BrokenBarrierException : 线程一直休眠直到被中断或 CyclicBarrier 内部计数器到达0,所有线程才继续运行
注 : 当很多线程在 await() 方法上等待的时候,其中一个线程被中断,这个线程将会抛出 InterruptedException 异常,其它线程抛出 BrokenBarrierException,于是 CyclicBarrier 对象就处于损坏状态
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException : 线程一直休眠直到被中断或 CyclicBarrier 内部计数器到达0或者指定时间过期,所有线程继续运行
public int getNumberWaiting() : 返回 await() 上阻塞的线程树木
public int getParties() : 返回 CyclicBarrier 对象同步的任务数
public void reset() : 重置发生后,await() 方法中等待的线程将收到一个 BrokenBarrierException 异常
public boolean isBroken() : 检测 CyclicBarrier 是否处于损坏状态
Phaser 类
允许并发多个阶段任务,当有并发任务且需要分解成及步执行时,该机制非常有用。Phaser类机制是在每一步结尾的位置对线程进行同步,当所有线程都完成这一步,才允许执行下一步。必须对 Phaser类中参数与同步操作的任务数进行初始化,可动态增加或者减少任务数
public Phaser(Phaser parent, int parties) : 通过父相位器创建一个新的相位器并注册未抵达数,当父相位器不为空且数大于0 则 这个子相位器被注册到父相位器上
public Phaser(int parties) : 使用给定数量的未抵达数创建一个新的相位器,此时无父相位器且初始化相位器数量为 0
public int arriveAndAwaitAdvance() : 抵达这个相位器并等待其它相位器抵达,当一个线程调用这个方法是 Phaser 对象减1 并把这个线程置于休眠状态直到其它线程完成这个过程,换句话说就是等待其它线程一起抵达再继续,可以位于不同的线程中但需要是同一个相位器
public int arrive() : 参与者完成任务后调用,这个方法通知 phaser 对象一个参与者已经完成当前阶段,但是他不应该等待其他参与者都完成当前阶段。必须小心使用这个方法,因为它不会与其它线程同步
public int arriveAndDeregister() : 任务完成,取消自己的注册,即这个线程完成当前语句后不会在下一个阶段中参与因而 phaser 对象在开始下一个阶段时不会等待这个线程,这是唯一减少参与者的方法
public int awaitAdvance(int phase) : 如果传入的阶段参数与当前阶段一致,这个方法会将当前线程置于休眠,直到这个阶段的所有参与者都运行完成。如果传入的阶段参数与当前阶段不一致,这个方法将立即返回
public int awaitAdvanceInterruptibly(int phase) throws InterruptedException : 和 awaitAdvance(int phase) 不同之处在于,如果在这个方法休眠的线程被中断,它将抛出 InterruptedException 异常
public int register() : 将一个新的参与者注册到 Phaser 中,这个新的参与者将被当成没有执行完本阶段的线程
public int bulkRegister(int parties) : 这个方法将指定数量的参与者注册到 Phaser 中,所有这些参与者都将被当成没有执行完本阶段的线程
public boolean isTerminated() : 检查 Phaser 是否终止
强制终止 Phaser
当一个 Phaser 对象没有参与线程的时候,它就处于终止状态。Phaser类提供 forceTermination()方法强制 phaser 进入终止态,这个方法不管 phaser 中是否存在注册的参与线程
当一个 phaser 处于终止状态的时候,arriveAdvance() 和 arriveAndAwaitAdvance() 方法立即返回一个负数
protected boolean onAdvance(int phase, int registeredParties) : 该方法不能直接调用,它在 phaser阶段改变的时候会被自动执行,其中的两个参数分别表示 当前阶段数 (基于0,之后逐步递增) 以及注册参与者数量,返回false表示 phaser 继续执行返回true表示phaser已经完成并进入终止状态,默认当参与者与注册者数量为0则返回true否则返回false
一个 Phaser 对象有两种状态 :
1> 活跃态(Active) : 当存在参与同步的线程的时候,Phaser 就是活跃的,并且在每个阶段结束的时候进行同步
2> 终止状态(Termination) : 当所有参与同步的线程都取消注册的时候,Phaser就处于终止状态,在这种情况下,Phaser没有任何参与者。更具体地说,当 Phaser 对象的 onAdvance() 方法返回 true的时候,Phaser 对象就处于终止态。通过覆盖这个方法可以改变默认的行为。当 Phaser 是终止态的时候,同步方法 arriveAndAwaitAdvance()会立即返回,而且不会做任何同步的操作
Phaser 类不必对其方法进行异常处理,当 Phaser类处于休眠状态不会相应中断事件就是说不会抛出 InterruptedException 异常
案例 : 三个任务在不同的文件夹及其子文件夹中查找过去24小时内修改过扩展名为 .log 的文件,这个任务分成以下三个步骤 :
1.在指定文件夹及其子文件夹中获得扩展名为 .log 的文件
2.对第一步的结果进行过滤,删除修改时间超过24小时的文件
3.将结果打印到控制台
public class
FileSearch
implements
Runnable {
private
String
initPath
;
private
String
end
;
private
List<String>
results
;
private
Phaser
phaser
;
public
FileSearch(String initPath, String end, Phaser phaser) {
this
.
initPath
= initPath;
this
.
end
= end;
this
.
phaser
= phaser;
results
=
new
ArrayList<>();
}
private void
directoryProcess(File file) {
File[] list = file.listFiles();
if
(list !=
null
) {
for
(
int
i =
0
; i < list.
length
; i++) {
if
(list[i].isDirectory()) {
directoryProcess(list[i]);
}
else
{
fileProcess(list[i]);
}
}
}
}
private void
fileProcess(File file) {
if
(file.getName().endsWith(
end
)) {
results
.add(file.getAbsolutePath());
}
}
private void
filterResults() {
List<String> newResults =
new
ArrayList<>();
long
actualDate =
new
Date().getTime();
for
(
int
i =
0
; i <
results
.size(); i++) {
File file =
new
File(
results
.get(i));
long
fileDate = file.lastModified();
if
(actualDate - fileDate < TimeUnit.
MILLISECONDS
.convert(
1
, TimeUnit.
DAYS
)) {
newResults.add(
results
.get(i));
}
}
results
= newResults;
}
private boolean
checkResults() {
if
(
results
.isEmpty()) {
System.
out
.printf(
"%s: Phase %d: 0 results.
\n
"
, Thread.
currentThread
().getName(),
phaser
.getPhase());
System.
out
.printf(
"%s: Phase %d: End.
\n
"
, Thread.
currentThread
().getName(),
phaser
.getPhase());
phaser
.arriveAndDeregister();
return false
;
}
else
{
System.
out
.printf(
"%s: Phase %d: %d results.
\n
"
, Thread.
currentThread
().getName(),
phaser
.getPhase(),
results
.size());
phaser
.arriveAndAwaitAdvance();
return true
;
}
}
private void
showInfo() {
for
(
int
i =
0
; i <
results
.size(); i++) {
File file =
new
File(
results
.get(i));
System.
out
.printf(
"%s: %s
\n
"
, Thread.
currentThread
().getName(), file.getAbsolutePath());
}
phaser
.arriveAndAwaitAdvance();
}
@Override
public void
run() {
phaser
.arriveAndAwaitAdvance();
System.
out
.printf(
"%s: Starting.
\n
"
, Thread.
currentThread
().getName());
File file =
new
File(
initPath
);
if
(file.isDirectory()) {
directoryProcess(file);
}
if
(!checkResults()) {
return
;
}
filterResults();
if
(!checkResults()) {
return
;
}
showInfo();
phaser
.arriveAndDeregister();
System.
out
.printf(
"%s: Work complete.
\n
"
, Thread.
currentThread
().getName());
}
public static void
main(String[] args) {
Phaser phaser =
new
Phaser(
3
);
FileSearch system =
new
FileSearch(
"C:
\\
Windows"
,
"log"
, phaser);
FileSearch apps =
new
FileSearch(
"C:
\\
Program Files"
,
"log"
, phaser);
FileSearch documents =
new
FileSearch(
"C:
\\
Documents And Settings"
,
"log"
, phaser);
Thread systemThread =
new
Thread(system,
"System"
);
systemThread.start();
Thread appsThread =
new
Thread(apps,
"Apps"
);
appsThread.start();
Thread documentThread =
new
Thread(documents,
"Documents"
);
documentThread.start();
try
{
systemThread.join();
appsThread.join();
documentThread.join();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
System.
out
.println(
"Terminated: "
+ phaser.isTerminated());
}
}
Exchanger 类
用于并发任务之间交换数据,具体说允许两个线程之间定义同步点。当两个线程都到达同步点时,它们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,同时第二线程的数据结构进入到第一个线程中
public Exchanger() : 创建一个数据交换者
public V exchange(V x) throws InterruptedException : 如果对方未到该方法,则等待知道对方抵达,之后交换数据并都继续运行
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException : 第一个参数时要交换的数据结构,该方法调用后线程休眠直到中断,或其它线程到达,或者已耗费指定时间,此时继续运行