本文是 《编写高质量代码之Java》 秦小波 成林 著、《Java JDK 7学习笔记》林信良著 关于线程部分的 学习笔记,特别是《Java JDK 7学习笔记》对于一些概念使用生活中的例子来解释,通俗易懂。
基本概念
线程:是进程中的执行流程。一个进程中可以包括多个线程。
进程:是一个包含自身执行地址的程序,在一个多任务的操作系统中,可以分配CPU时间给每一个进程,CPU在某个时间片中执行某个进程,然后下一个时间片执行另一个进程,由于转换速度很快,使得每个程序好像是在同时进行处理。
线程入门
继承Thread
/**
* 文本模式下的密码屏蔽线程
* @author Administrator
*
*/
public class EraserThread extends Thread{
private boolean active;
public void setActive(boolean active) {
this.active = active;
}
private String mask;
public EraserThread() {
this('*');
}
public EraserThread(char c) {
active =true;
mask = "\010" + mask;
}
public boolean isActive(){
return active;
}
@SuppressWarnings("static-access")
@Override
public void run() {
while (isAlive()) {
try {
Thread.currentThread().sleep(50);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 基本上建议以实现 Runable的方法让对象具有线程功能,保留日后的修改弹性。
* @author Administrator
*
*/
public class EraserThreadDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("输入名称");
String name = scanner.next();
System.out.println("输入密码");
//启动Eraser 线程
EraserThread eraserThread = new EraserThread('#');
eraserThread.start();
String password = scanner.next();
eraserThread.setActive(false);
if("wang".equals(name) && "123".equals(password)){
System.out.println("欢迎光临");
break;
}else{
System.out.println(eraserThread.isActive());
System.out.printf("%s,名称或密码错误,请重新输入!%n",name );
}
}
}
}
实现Runnable
基本上建议以实现 Runable的方法让对象具有线程功能,保留日后的修改弹性。
/**
* 文本模式下的密码屏蔽线程
* @author Administrator
*
*/
public class EraserThreadRunnable implements Runnable{
private boolean active;
public void setActive(boolean active) {
this.active = active;
}
private String mask;
public EraserThreadRunnable() {
this('*');
}
public EraserThreadRunnable(char c) {
active =true;
mask = "\010" + mask;
}
public boolean isActive(){
return active;
}
@SuppressWarnings("static-access")
@Override
public void run() {
while (isActive()) {
try {
Thread.currentThread().sleep(50);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 基本上建议以实现 Runable的方法让对象具有线程功能,保留日后的修改弹性。
* @author Administrator
*
*/
public class EraserThreadDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("输入名称");
String name = scanner.next();
System.out.println("输入密码");
//启动Eraser 线程
EraserThread eraserThread = new EraserThread('#');
Thread thread = new Thread(eraserThread);
thread.start();
String password = scanner.next();
eraserThread.setActive(false);
if("wang".equals(name) && "123".equals(password)){
System.out.println("欢迎光临");
break;
}else{
System.out.println(eraserThread.isActive());
System.out.printf("%s,名称或密码错误,请重新输入!%n",name );
}
}
}
}
Daemon线程
Daemon线程:在后台执行服务的线程,例如垃圾收集线程
如果所有的非Daemon线程都结束了,则Daemon线程就会自动终止。
Java默认所有从Daemon线程产生的线程也是Daemon线程,因为一个后台服务线程衍生出来的线程,也应该是为了后台服务而产生的,所以在产生它的线程停止后,衍生线程也应该跟着停止。
/**
* Daemon 线程是在后台执行的服务线程。如果所有的非daemon 结束,则Daemon 线程结束。
* @author Administrator
* main 开始是一个非Daemon 线程, 设置 thread.setDaemon(true);在这个线程的主线程结束后,Daemon 线程 也跟着结束了。
*
*
*/
public class DaemomThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
System.out.println("T");
}
}
});
thread.setDaemon(true);
thread.start();
}
}
线程的生命周期
线程的几个状态:线程的创建(new Thread)、就绪(Runnable)、堵塞(Blocked)、消亡(Dead)。
当新建一个Thread 并执行start()之后,线程进入Runnable状态,此时线程尚未开始执行,必须等待调度器(Schedule)的调度,被排入的线程才会执行run()方法中的定义。
线程优先级
从1到10,默认是5,设置setPriority()
/**
* 本文是 编写高质量代码之Java 秦小波 成林 著
* 建议121:线程优先级只使用三个等级 学习笔记
* @author Administrator
*线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小。
*Java的线程有10个级别(准确地说是11个级别,
*级别为0的线程是JVM的,应用程序不能设置该级别),
*那是不是说级别是10的线程肯定比级别为9的线程先运行呢
*/
public class Priority {
public static void main(String[] args) {
//启动20多个不同优先级的线程
for (int i = 0; i < 10; i++) {
new TestThread().start(i%10+1);
}
}
}
/**
* 该多线程类实现了Runnable接口,实现了run方法,注意在run方法中有一个比较占用CPU的计算,该计算毫无意义,
* 只是为了保证一个线程尽可能多地消耗CPU资源,目的是为了观察CPU繁忙时不同优先级线程的执行顺序。需要说明的是,
* 如果此处使用了Thread.sleep()方法,
* 则不能体现出线程优先级的本质了,因为CPU并不繁忙,线程调度不会遵循优先级顺序来进行调度。
* @author Administrator
*
*/
class TestThread implements Runnable{
@Override
public void run() {
//消耗CPU的计算,性能差的机器,请修改循环限制
for (int i = 0; i < 10000; i++) {
Math.hypot(Math.pow(924526789, i), Math.cos(i));
}
//输出线程优先级
System.out.println("Priority : "+Thread.currentThread().getPriority());
}
//启动线程
public void start(int priority){
Thread t = new Thread(this);
//设置线程的优先级
t.setPriority(priority);
t.start();
}
}
/**
* Priority : 8
Priority : 1
Priority : 3
Priority : 4
Priority : 9
Priority : 10
Priority : 5
Priority : 7
Priority : 6
Priority : 2
(1)并不是严格遵照线程优先级别来执行的
比如线程优先级为9的线程可能比优先级为10的线程先执行,优先级为1的线程可能比优先级为2的线程先执行,
但很少会出现优先级为2的线程比优先级为10的线程先执行(这里用了一个词“很少”,是说确实有可能出现,
只是几率非常低,因为优先级只是表示线程获得CPU运行的机会,并不代表强制的排序号)。
(2)优先级差别越大,运行机会差别越明显
比如优先级为10的线程通常会比优先级为2的线程先执行,
但是优先级为6的线程和优先级为5的线程差别就不太明显了,
执行多次,你会发现有不同的顺序。
这两个现象是线程优先级的一个重要表现,之所以会出现这种情况,是因为线程运行是需要获得CPU资源的,
那谁能决定哪个线程先获得哪个线程后获得呢?这是依照操作系统设定的线程优先级来分配的,也就是说,
每个线程要运行,需要操作系统分配优先级和CPU资源,对于Java来说,JVM调用操作系统的接口设置优先级,
比如Windows操作系统是通过调用SetThreadPriority函数来设置的,
问题来了:不同的操作系统线程优先级都相同吗?
这是个好问题。事实上,不同的操作系统线程优先级是不相同的,Windows有7个优先级,
Linux有140个优先级,Freebsd则有255个(此处指的是优先级总数,不同操作系统有不同的分类,
如中断级线程、操作系统级等,各个操作系统具体用户可用的线程数量也不相同)。Java是跨平台的系统,
需要把这个10个优先级映射成不同操作系统的优先级,于是界定了Java的优先级只是代表抢占CPU的机会大小,
优先级越高,抢占CPU的机会越大,被优先执行的可能性越高,优先级相差不大,则抢占CPU的机会差别也不大,
这就是导致了优先级为9的线程可能比优先级为10的线程先运行。
Java的缔造者们也察觉到了线程优先问题,于是在Thread类中设置了三个优先级,此意就是告诉开发者,
建议使用优先级常量,而不是1到10随机的数字。常量代码如下:
/**
* The minimum priority that a thread can have.
*
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*
public final static int MAX_PRIORITY = 10;
在编码时直接使用这些优先级常量,可以说在大部分情况下MAX_PRIORITY的线程会比NORM_PRIORITY的线程先运行,
但是不能认为必然会先运行,不能把这个优先级做为核心业务的必然条件,Java无法保证优先级高肯定会先执行,
只能保证高优先级有更多的执行机会。因此,建议在开发时只使用此三类优先级,没有必要使用其他7个数字,
这样也可以保证在不同的操作系统上优先级的表现基本相同。
明白了这个问题,那可能有读者要问了:如果优先级相同呢?这很好办,也是由操作系统决定的,
基本上是按照FIFO原则(先入先出,First Input First Output),但也是不能完全保证。
注意 线程优先级推荐使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三个级别,
不建议使用其他7个数字。
*/
yield()
绝大数的操作系统都支持TimesLicing,简单的说就是操作系统会为每个线性分配一段CPU时间(Quantum),时间一到就换下一个线程,即使现有的线程还没有结束。对于不支持TimesLicing的操作系统可以使用yield()礼让一下其他线程。
yield()方法让拥有同样优先级的线程有被执行的机会,当线程执行yield()方法让出执行权时,他会再度处于Runnable状态,等待调度。这里有点像让梨的孔融啊!!!
对于支持TimesLicing的操作系统不用使用yield(),操作系统会自动分配时间给线程来轮流执行
Blocked状态
线程调度器将不会为Blocked状态的线程分配执行时间,直到线程回到Runnable状态。
有几种状况会让线程进入Blocked状态:
- 等待输入的完成
- 调用sleep()方法
- 尝试获得对象锁定
- 调用wait()方法
有几种状况会让线程进入Runable状态
- 输出完成
- 调用interrup()
- 获得对象锁定
- 调用notify()或者notifyAll()
Dead
如果执行完成(或发生异常)而离开run方法,则线程执行完毕,进入Dead状态,可以使用isAlive()查看线程时候还活着。
书中举了一个例证,使用Thread.sleep(99999);进入Blocked状态,使用thread.interrupt();离开Blocked状态,但是会抛出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.InterruptDemo$1.run(InterruptDemo.java:11)
at java.lang.Thread.run(Unknown Source)
I'm interrupted !
public class InterruptDemo {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//暂停 99999
try {
Thread.sleep(99999);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("I'm interrupted !");
}
}
});
thread.start();
thread.interrupt();
}
}
暂停时间未知时线程暂停的好方法
如果要暂停线程,但是暂停时间未知,使用sleep()并不是一个好方法,可以使用wait()让线程进入Blocked状态,然后让别的线程用notify()或notifyAll()来通知被Blocked的线程回到Runnable状态
。
线程的加入(join) 类似于插队
如果有一个线程A正在运行,你希望插入一个线程B,并且要求线程B先执行完毕,然后再继续A线程的流程,可以使用join()来完成。这就好比就你一个人搞安卓,你现在正在做项目A,突然有客户反映你做的项目B一直崩溃,这时你的经理过来对你说,把手头的工作都停了,马上改项目B,哈哈哈。
有时加入的线程可能处理太久,你不能无止境的等待这个线程,则可以在join()上加上时间,例如join(1000),表示加入的这个线程至多处理1000毫秒,如果过了时间还没处理完,就继续执行之前的工作流程。好比你是搞软件的,销售部的电脑蓝屏了,非要来叫你去修,你不太愿意的去了,你打开机箱,拔下内存条,用橡皮插了插,重启不行,你又拆下硬板线,又试了一下,还不行,你有打开手机把报错的字母百度了一下,还是不行,一看表还要10分钟就要下班了,你机智无奈的低下了头,说我修不了。然后回去写代码了。
/**
* jion 类似于插队
* @author Administrator
*Thread A 执行
线程B 开始
线程B 执行。。
线程B 执行。。
线程B 执行。。
线程B 执行。。
线程B 执行。。
线程B 即将结束
Thread A 执行
*/
public class JoinDemo {
public static void main(String[] args) {
System.out.println("Thread A 执行");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程B 开始");
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println("线程B 执行。。");
}
System.out.println("线程B 即将结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
};
}
});
threadB.start();
try {
threadB.join();
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("Thread A 执行");
}
}
线程的停止
不建议使用stop()来停止线程。
如果要停止一个线程,就是要提供一个方法让线程可以完成run()方法。
标志位
public class StopThread implements Runnable{
//标志位
private boolean isContinue = true;
public void setContinue(boolean isContinue) {
this.isContinue = isContinue;
}
@Override
public void run() {
while(isContinue){
}
}
}
下边的和Dead 部分是同一个内容:
如果执行完成(或发生异常)而离开run方法,则线程执行完毕,进入Dead状态,可以使用isAlive()查看线程时候还活着。
书中举了一个例证,使用Thread.sleep(99999);进入Blocked状态,使用thread.interrupt();离开Blocked状态,但是会抛出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.InterruptDemo$1.run(InterruptDemo.java:11)
at java.lang.Thread.run(Unknown Source)
I'm interrupted !
public class InterruptDemo {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//暂停 99999
try {
Thread.sleep(99999);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("I'm interrupted !");
}
}
});
thread.start();
thread.interrupt();
}
}
ThreadGroup
Java中每个线程都属于某个线程组(ThreadGroup)
可以使用下面的语句获取属于哪个线程组:
Thread.currentThread().getThreadGroup().getName()
uncaughtException()方法。当线程组中的线程发生uncaughtException异常时,由此方法来进行相关处理。
public class ThreadGroupDemo {
public static void main(String[] args) {
ThreadGroup trGroup1 = new ThreadGroup("trGroup1");
ThreadGroup trGroup2 = new ThreadGroup("trGroup2");
Thread thread1 = new Thread(trGroup1,"trGroup1 munber");
Thread thread2 = new Thread(trGroup2,"trGroup2 munber");
Thread[] threads = new Thread[trGroup1.activeCount()];
//如果想获取线程组中的所有数据来进行操作,可以使用enumerate方法
trGroup1.enumerate(threads);
//interrupt所有线程
trGroup1.interrupt();
trGroup1.setMaxPriority(10);
System.out.println(Thread.currentThread().getThreadGroup().getName());
System.out.println(Thread.currentThread().getThreadGroup().activeCount());
final ThreadGroup trGroup = new ThreadGroup("group1"){
@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
System.out.println(t.getName()+" :"+e.getMessage());
}
};
//这是匿名类的写法
Thread thread = new Thread(trGroup,new Runnable() {
@Override
public void run() {
//抛出异常
throw new RuntimeException("测试异常");
}
});
thread.start();
}
}
线程异常处理
在JDK5以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。
如果ThreadGroup中的线程发生了异常,uncaughtException()方法处理顺序是
如果ThreadGroup 有父ThreadGroup,就会调用父threadGroup的uncaughtException()方法.
如果,Thread 设置了setUncaughtExceptionHandler(),就调用其uncaughtException()方法
看是不是ThreadDeat实例,若是什么都不做,若否则调用异常的printStrackTrace()
/**
* 本文是 编写高质量代码之Java 秦小波 成林 著 建议
* 建议122:使用线程异常处理器提升系统可靠性 学习笔记
* @author Administrator
*Java 1. 5版本以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。
*1.若要在实际环境中应用,则需要注意以下三个方面:
*(1)共享资源锁定
*如果线程异常产生的原因是资源被锁定,自动重启应用只会增加系统的负担,无法提供不间断服务。例如一个即时通信服务器(XMPP Server)
*出现信息不能写入的情况时,即使再怎么重启服务,也是无法解决问题的。
*在此情况下最好的办法是停止所有的线程,释放资源。
*(2)脏数据引起系统逻辑混乱
*异常的产生中断了正在执行的业务逻辑,特别是如果正在执行一个原子操作(像即时通信服务器的用户验证和签到这两个事件应该在一个操作中处理,
*不允许出现验证成功但签到不成功的情况),但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,
*例如出现用户认证通过了,但签到不成功的情况,在这种情景下重启应用服务器,
*虽然可以提供服务,但对部分用户则产生了逻辑异常。
*(3)内存溢出
*线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批新对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,
*重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在此种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,
*但是如果线程反复重启,很可能会引起OutOfMemory内存泄露问题。
*/
public class UseThreadExceptionHandlerToImproveSystemReliability {
public static void main(String[] args) {
new Thread(new TcpService()).start();
}
}
class TcpService implements Runnable{
//创建后即运行
@Override
public void run() {
//正常业务流程,运行3秒
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
System.out.println("系统正常运行 "+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抛出异常
throw new RuntimeException();
}
public TcpService(){
Thread t= new Thread();
t.setUncaughtExceptionHandler(new TcpServiceExceptionHandler());
t.start();
}
}
import java.lang.Thread.UncaughtExceptionHandler;
/**
* (1)那TcpServerExceptionHandler异常处理器做什么事呢?两件事:
* (2)重新启动一个新线程,提供不间断的服务。
* @author Administrator
*
*/
public class TcpServiceExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
//记录线程异常信息
System.out.println("线程"+t.getName()+"出现异常,自行重启,请分析原因");
e.printStackTrace();
//重启线程,保证业务部中断
new TcpService();
}
}
synchronized
每个对象都会有个一个内部锁定(IntrinsicLock),或者称为监控锁定(Monitor)。被标记为synchronized的区块将会被监控,任何线程要执行syncronized区块都必须取得指定的对象锁。
上边的说的比较学术,通俗的讲,就比如一个房间里有5个人,只有一个卫生间,假如A占用了从内部锁住门(获得锁),B也急着上就只能等了。
线程若尝试执行syncronized区块而进入Blocked,在取得锁之后,会先到Runable状态,等待CPU排班器排入Running状态
使用方式
声明在方法上
声明在区块上,优点减少锁定范围,提高效率
public test add(Object obj){
syncronized(this){
}
}
Lock
/**
* 本文是 编写高质量代码之Java 秦小波 著 建议127:Lock与synchronized是不一样的 学习笔记
*
* @author Administrator
*
*/
public class LockAndSynchronized {
public static void runTasks(Class<? extends Runnable> clz)
throws Exception, IllegalAccessException {
ExecutorService es = Executors.newCachedThreadPool();
System.out.println("*****开始执行" + clz.getSimpleName() + "任务***");
// 启动三个线程
for (int i = 0; i < 3; i++) {
es.submit(clz.newInstance());
}
// 等待足够长的时间,然后关闭执行器
TimeUnit.SECONDS.sleep(10);
System.out.println("---------------" + clz.getSimpleName()
+ "任务执行完毕-----\n");
// 关闭执行器
es.shutdown();
}
public static void main(String[] args) throws IllegalAccessException,
Exception {
// 运行显式锁任务
runTasks(TaskWithLock.class);
runTasks(TaskWithSync.class);
// 多个线程共享锁
final Lock lock = new ReentrantLock();
// 启动三个线程
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
// 休眠2秒钟
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
}
}
}
/***
* *****开始执行TaskWithLock任务*** 线程名称: pool-1-thread-3,执行时间:34s 线程名称:
* pool-1-thread-1,执行时间:34s 线程名称: pool-1-thread-2,执行时间:34s
* ---------------TaskWithLock任务执行完毕-----
*
***** 开始执行TaskWithSync任务*** 线程名称: pool-2-thread-1,执行时间:44s 线程名称:
* pool-2-thread-3,执行时间:45s 线程名称: pool-2-thread-2,执行时间:46s
* ---------------TaskWithSync任务执行完毕-----
* Thread-0
Thread-1
Thread-2
* 称Thread-0、Thread-1、Thread-2会逐渐输出,也就是一个线程在执行时,其他线程就处于等待状态。
* 注意,这里三个线程运行的实例对象是同一个类
* 注意看运行的时间戳,显式锁是同时运行的,很显然在pool-1-thread-1线程执行到sleep时,其他两个线程也会运行到这里,
* 一起等待,然后一起输出,这还具有线程互斥的概念吗?
* 而内部锁的输出则是我们的预期结果:pool-2-thread-1线程在运行时其他线程处于等待状态,pool-2-thread-1执行完毕后,
* JVM从等待线程池中随机获得一个线程pool-2-thread-3执行,最后再执行pool-2-thread-2,这正是我们希望的。
*
* 而内部锁的输出则是我们的预期结果:pool-2-thread-1线程在运行时其他线程处于等待状态,pool-2-thread-1执行完毕后,
* JVM从等待线程池中随机获得一个线程pool-2-thread-3执行,最后再执行pool-2-thread-2,这正是我们希望的。
* 现在问题来了:Lock锁为什么不出现互斥情况呢?
* 这是因为对于同步资源来说(示例中是代码块),显式锁是对象级别的锁,而内部锁是类级别的锁,也就是说Lock锁是跟随对象的
* ,synchronized锁是跟随类的, 更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程的共享变量。
*
* 显式锁和内部锁还有什么不同呢?还有以下4点不同:
* (1)Lock支持更细粒度的锁控制
* (2)Lock是无阻塞锁,synchronized是阻塞锁
*
* 当线程A持有锁时,线程B也期望获得锁,此时,如果程序中使用的是显式锁,
* 则B线程为等待状态(在通常的描述中,也认为此线程被阻塞了),若使用的是内部锁则为阻塞状态。
* (3)Lock可实现公平锁,synchronized只能是非公平锁
* 什么叫非公平锁呢?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁,JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,
* 这叫做非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁(保证每个线程的等待时间均衡)。需要注意的是,
* 即使是公平锁,JVM也无法准确做到“公平”,在程序中不能以此作为精确计算。
* 显式锁默认是非公平锁,但可以在构造函数中加入参数true来声明出公平锁,而synchronized实现的是非公平锁,
* 它不能实现公平锁。
* (4)Lock是代码级的,synchronized是JVM级的
* Lock是通过编码实现的,synchronized是在运行期由JVM解释的,相对来说synchronized的优化可能性更高,
* 毕竟是在最核心部分支持的,Lock的优化则需要用户自行考虑。
* 显式锁和内部锁的功能各不相同,在性能上也稍有差别,但随着JDK的不断推进,相对来说,显式锁使用起来更加便利和强大,
* 在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:
* 灵活、强大则选择Lock,快捷、安全则选择synchronized。
* 注意 两种不同的锁机制,根据不同的情况来选择。
*/
/**
* 该类模拟了一个执行时间比较长的计算,注意这里使用的是模拟方式, 在使用sleep方法时线程的状态会从运行状态转变为等待状态。
* 该任务要具备多线程能力时必须实现Runnable接口
*
* @author Administrator
*
*/
class Task {
public void doSomething() {
try {
// 每个线程等待2秒钟,注意将此时的线程转为WAITING状态
Thread.sleep(1000);
} catch (Exception e) {
// 异常处理
}
StringBuffer sb = new StringBuffer();
sb.append("线程名称: " + Thread.currentThread().getName());
sb.append(",执行时间:" + Calendar.getInstance().get(13) + "s");
System.out.println(sb);
}
}
class TaskWithLock extends Task implements Runnable {
// 声明显示锁
private final Lock lock = new ReentrantLock();
@Override
public void run() {
/**
* 显式锁的锁定和释放必须在一个try……finally块中,这是为了确保即使出现运行 期异常也能正常释放锁,保证其他线程能够顺利执行。
*/
try {
// 开始锁定
lock.lock();
doSomething();
} catch (Exception e) {
} finally {
// 释放锁
lock.unlock();
}
}
}
class TaskWithSync extends Task implements Runnable {
@Override
public void run() {
// 内部锁
synchronized ("A") {
doSomething();
}
}
}
/**
* (1)Lock支持更细粒度的锁控制
* 可以编写一个Runnable的实现类,把Foo类作为资源进行调用(注意多线程是共享这个资源的),
* 然后就会发现这样的现象:读写锁允许同时有多个读操作但只允许有一个写操作,也就是当有一个写线程在执行时,
* 所有的读线程和写线程都会阻塞,直到写线程释放锁资源为止,而读锁则可以有多个线程同时执行。
* @author Administrator
*
*/
class Foo{
//可重入的读写锁
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//读锁
private final Lock r =rwl.readLock();
//写锁
private final Lock w = rwl.writeLock();
//多操作,可并发执行
public void read(){
try {
r.lock();
Thread.sleep(1000);
System.out.println("read ....");
} catch (Exception e) {
e.printStackTrace();
}finally{
r.unlock();
}
}
//写操作,同时只允许一个写操作
public void write(Object object){
try {
w.lock();
Thread.sleep(1000);
System.out.println("Writing.....");
} catch (Exception e) {
e.printStackTrace();
}finally{
w.unlock();
}
}
}
volatile
/**
* volatile
* @author Administrator
* 本文是 编写高质量代码之Java 秦小波 成林 著
*建议123:volatile不能保证数据同步 学习笔记
*volatile关键字比较少用,原因无外乎两点,
*一是在Java 1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;
*二是比较难设计,而且误用较多,这也导致它的“名誉”受损。
*
*每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,
*比如寄存器Register、高速缓冲存储器Cache等),线程的计算一般是通过工作内存进行交互的
*
* ******************************
* * 线程体 *
* ***************************▲
* 3 线程写入 | | 2 线程读取
* ▼
* *******************************
* * 工作内存 *
* *******************************
* 4 写入数据 ▼ ▲ 1. 读取加载
***************************************************************
* 主内存 *
* *
*************************************************************
*
*从示意图上我们可以看到,线程在初始化时从主内存中加载所需的变量值到工作内存中,然后在线程运行时,如果是读取,
*则直接从工作内存中读取,若是写入则先写到工作内存中,之后再刷新到主存中,这是JVM的一个简单的内存模型,
*但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主存中,
*但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最“新鲜”的值,此时就出现了不同
*线程持有的公共资源不同步的情况。
*对于此类问题有很多解决办法,比如使用synchronized同步代码块,或者使用Lock锁来解决该问题,不过,
*Java可以使用volatile更简单地解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个
*线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获
*得最“新鲜”的变量值
* ******************************
* ←← ←← ←← ←← * 线程体 *
* ↓ ***************************▲ ← ← ← ←↑
* ↓ 3 线程写入 | | 2 线程读取 ↑
* ↓ ▼ ↑volatile 变量读取
* ↓ ******************************* ↑
* ↓ * 工作内存 * ↑
* volatile写入 ******************************* ↑
* 4 写入数据 ▼ ▲ 1. 读取加载 ↑↑
***************************************************************
* 主内存 *
* *
*************************************************************
*
*注意 volatile不能保证数据是同步的,只能保证线程能够获得最新值。
*/
public class Volatile {
public static void main(String[] args) throws Exception {
//理想值,并作为最大循环次数
int value=1000;
//循环次数,防止出现无限循环造成死机的情况
int loops=0;
//主线程,用于估计活动线程数
ThreadGroup tg = Thread.currentThread().getThreadGroup();
while (loops++<value) {
//共享资源清零
UnsafeThread ut = new UnsafeThread();
for (int i = 0; i < value; i++) {
new Thread(ut).start();
}
//先等15毫秒,等待活动线程数量成为1
do {
Thread.sleep(15);
} while (tg.activeCount()!=1);
//检查实际值与理论值是否一致
if(ut.getCount()!=value){
//出现线程不安全的情况
System.out.println("循环到第 "+loops+" 遍,出现线程不安全情况");
System.out.println("此时,count="+ut.getCount());
System.exit(0);
}
}
}
}
/**这是一段设计很巧妙的程序,要让volatile变量“出点丑”还是需要花点功夫的。此段程序的运行逻辑如下: 启动100个线程,
* 修改共享资源count的值。 暂停15毫秒,观察活动线程数是否为1(即只剩下主线程在运行),若不为1,则再等待15毫秒。
* 判断共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。
* 如果没有找到,继续循环,直到达到最大循环次数为止。运行结果如下:
*输出:
* 循环到第 21 遍,出现线程不安全情况
此时,count=999
这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,
在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程安全。
这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,
count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程安全。
在解释原因之前,我们先说一下自加操作。count++表示的是先取出count的值然后再加1,
也就是count=count+1,所以,在某两个紧邻的时间片段内会发生如下神奇的事情:
(1)第一个时间片段
A线程获得执行机会,因为有关键字volatile修饰,所以它从主内存中获得count的最新值998,
接下来的事情又分为两种类型:
如果是单CPU,此时调度器暂停A线程执行,出让执行机会给B线程,于是B线程也获得了count的最新值998。
(2)第二个时间片段
如果是单CPU, B线程执行完加1动作(这是一个原子处理),count的值为999,由于是volatile类型的变量,所以直接写入主内存,
然后A线程继续执行,计算的结果也是999,重新写入主内存中。
如果是多CPU, A线程执行完加1动作后修改主内存的变量count为999,线程B执行完毕后也修改主内存中的变量为999。
这两个时间片段执行完毕后,原本期望的结果为1000,但运行后的值却为999,这表示出现了线程不安全的情况。
这也是我们要说明的:volatile关键字并不能保证线程安全,
它只能保证当线程需要该变量的值时能够获得最新的值,而不能保证多个线程修改的安全性。
顺便说一下,在上面的代码中,UnsafeThread类的消耗CPU计算是必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,
否则很难模拟出volatile线程不安全的情况,读者可以自行模拟测试。
*/
/**
* 定义了一个多线程类,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,
* 确保是从主内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加动作,count变量会产生脏数据吗
* @author Administrator
*
*/
class UnsafeThread implements Runnable{
//共享资源
private volatile int count =0;
@Override
public void run() {
//增加CPU的繁忙程度,不用关心其逻辑含义
for (int i = 0; i < 10000; i++) {
Math.hypot(Math.pow(924526789, i), Math.cos(i));
}
//自增运算
count++;
}
public int getCount(){
return count;
}
}
wait()、notify()、notifyAll()
wait()、notify()、notifyAll()是Object定义的方法,通过这三个方法控制线程释放对象锁定,或者通知线程参与锁定竞争。
线程调用对象的wait()方法时,会让出syncronized区块使用权并等待通知,或者等待指定时间,直到被notyfy()或时间到时,(取得对象锁定之后)再调用wait()处开始执行。这就好比你去柜台办事,做到一半时柜台人员叫你到等候区等候通知(或者等2分钟子类的),当你被通知(或者时间到时),柜台人员才会继续为你服务。如果有多人同时等待,调用notifyAll()就相当于通知等待区的所有人,看谁先抢到第一名,就先处理谁的业务。
最常用的例子,就是生产者消费者问题。