前言
此文为学习葛一鸣、郭超的《实战Java高并发程序设计》的学习笔记
线程是什么?与进程有什么关系?
在电脑中,一个可以运行的文件,都是程序,但是他们都是“死的”,静态。运行之后,他就会变成一个进程,此时就“活了”。在任务管理器中,一个个动态运行的就是进程。
进程中包含多个线程,进程就像一个机器,线程就是机器的各个零件,他们要一起分工合作才能保证这个进程顺利运行。
Java实现
在Java语言中,新建一个线程,需要new一个Thread对象
运行一个线程则是通过start()
Thread t = new Thread();
t.start();
线程在start()之后会运行run()方法,但是直接使用run()仅仅是作为一个普通的方法调用,却不能新建一个线程。如下代码
Thread t = new Thread();
t.run();
创建一个线程
- 可以通过继承Thread,重载run()方法实现
public class extendThread extends Thread{
public void run() {
System.out.println("Hello");
}
}
- 实现Runnable接口
public class extendThread implements Runnable {
@Override
public void run() {
System.out.println("Hello");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new extendThread());
t.start();
}
}
继承Thread和实现Runnable接口都是常用的方法,但是实现建议Runnable接口,因为可以避免单继承的限制,比较灵活。
终止线程 stop()
终止线程不建议使用stop(),因为stop()是强制将一个线程终止,不管这个线程此时是否将程序执行完了,如果是写操作,这可能会导致写到一半,剩下一半没有写完的情况。如下
id = 001, name = 张三, sex = 男
如果需要修改为
id = 001, name = 李四, sex = 女
写入了name值之后,被stop()方法强制结束了,可能导致这样的后果
id = 001 , name = 李四, sex = 男
sex值没有修改,这样子的错误是致命的
线程中断
在Thread中有三个关于线程中断的方法
public void interrupt() //中断线程
public boolean isInterrupted() //判断是否被中断
public static boolean interrupted() //判断是否被中断,并清除中断状态
Thread.interrupt():将一个进程设置中断标志位,中断标志位表示当前线程已经被中断了
Thread.isInterrupted():判断当前线程是否有被中断(通过检查中断标志位)
Thread.interrupted():判断当前线程的中断状态,并清除当前线程的中断标志位状态
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new extendThread());
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
还是以上面实现Runnable接口的extendThread类为例,运行上面的程序依然打印出结果
上面的程序虽然有Thread.interrupt()中断方法,中断没有发生任何作用,因为Thread.interrupt()只是设置了中断标志位,而在实例对象中没有处理中断的逻辑代码
public class extendThread implements Runnable {
int index = 1;
@Override
public void run() {
while(true) {
if(Thread.currentThread().isInterrupted()) { //判断
System.out.println("Interrupted");
break;
}
System.out.println("Hello" + index++);
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new extendThread());
t.start();
Thread.sleep(200);
t.interrupt();
}
}
运行结果:
…(省略)
Hello14406
Hello14407
Hello14408
Hello14409
Hello14410
Interrupted
Thread.sleep()方法会让当前的线程休眠。它会抛出一个异常,并且需要去处理这个异常
等待(wait)和通知(notify)
wait()和notify()是某一个对象上使用的两个方法
线程A在一个对象上使用了obj.wait(),当前线程就是在这个对象的使用上进行等待,我的理解是交出了对象的控制权
直到某个线程X使用了obj.notify()为止
但是如果有多个线程使用了obj.wait(),即是有对个线程等待对象的控制权,notify()会在等待队列中 随机 选一个唤醒
wait()方法和notify()方法需要在synchronized(object){};语句中使用
public class SimleWN {
final static Object object = new Object();
public static class T1 extends Thread{
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T1 start! ");
try {
System.out.println(System.currentTimeMillis() + ":T1 wait for object ");
//交出object控制权
object.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ":T1 end! ");
}
}
}
public static class T2 extends Thread{
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T2 start! ");
//唤醒其中一个
object.notify();
System.out.println(System.currentTimeMillis() + ":T2 end! ");
try {
Thread.sleep(2000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
结果:
1554044597679:T1 start!
1554044597680:T1 wait for object
1554044597680:T2 start!
1554044597680:T2 end!
1554044599681:T1 end!
上面的代码执行过程:当 t1 开始执行run()方法打印
1554044597679:T1 start!
1554044597680:T1 wait for object
之后,开始交出对象控制权,在此之前 t2 线程一直处于等待状态,当 t1 得到对象控制权之后,马上开始打印
1554044597680:T2 start!
之后开始唤醒 t1 线程,但是 t1 线程却不能立刻开始运行,因为此时的对象控制权还没有交出来所以 t1 还是处于等待的状态
当 t2 打印
1554044597680:T2 end!
并睡眠2秒后(可以看到时间戳),结束程序,即交出的对象控制权
此时 t1 获取对象控制权,开始打印最后一句
1554044599681:T1 end!
挂起(suspend)和继续执行(resume)
这一对方法与wait()、notify()很像
suspend()是将线程挂起,必须等待resume()才可以继续执行
区别在于,suspend()不会释放任何锁资源,它仅仅只是暂停运行了而已,这样就容易出现一个问题,如果resume()在suspend()之前执行,会导致被永远挂起,锁也是不会释放
public class BadSuspend {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread{
public ChangeObjectThread(String name) {
super.setName(name);
}
public void run() {
synchronized (u){
System.out.println("in "+ getName());
Thread.currentThread().suspend();
}
}
}
// 导致resume不生效的执行顺序可能是这样的:
// 打印t1 => t1在suspend => t2等待u释放 => t1被resume => t2被resume => u释放打印t2 => t2被suspend => 永远无法结束
public static void main(String []args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}
因为是在synchronized语句中执行,所以当 t1 开始线程之后就获得了对象锁
因为主线程睡眠了0.1秒,当 t2 开始线程之后,t1 已经挂起了,所以 t2 是一直是就绪状态等待对象锁
因为主线程和 t1 线程在运行上几乎是同时,也可以想象成 t1.resume()、t2.resume()是同时执行
此时 t1 线程也是同时执行,也就是说,当主线程执行t2.resume()时,t1线程还没有执行完,没有释放对象锁
所以 t2 线程是在主线程执行了t2.resume()之后才被挂起的,而此时就被永远的挂起了
等待线程结束(join)和谦让(yield)
如果在A线程中调用了B.join()方法,A线程就会等待B线程结束之后才结束
还可以在join()方法加入参数,如join(1000),意味着等待一秒之后如果还没有结束就不等
public class JoinMain {
public static volatile int i = 0;
public static class AddThread extends Thread{
public void run() {
for(i=0;i<10000000;i++){
}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
输出结果总是10000000
如果不是调用了join(),at线程可能没有执行完,主线程就将 i 打印出来了
join()的核心代码
while(isAlive){
wait(0);
}
当线程完成之后,被等待的线程会在退出前调用notifyAll()通知所有等待线程继续执行
所以要尽量的避免在Thread对象实例上使用wait()或者notify()方法,因为这很有可能会影响系统API的工作,或者被系统API所影响
yield()方法会是当前线程让出CPU,之后处于就绪的状态,也就是说很有可能这个线程会继续抢到CPU资源
volatile与Java内存模型(JMM)
Java内存模型说的是原子性、有序性和可见性
volatile的存在就是用来确保线程间的可见性
//可见性
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run() {
while(!ready);
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(1000);
}
}
上述代码中,主线程修改ready的值,ReaderThread线程是无法看到的,所以也就是永远无法打印出number,但是如果加上volatile,就可以解决这个问题
线程组
线程组可以将功能相同的线程分组
public class ThreadGroupName implements Runnable {
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName() + Thread.currentThread().getName();
while (true){
System.out.println(groupAndName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
//创建一个叫PrintGroup的线程组
ThreadGroup tg = new ThreadGroup("PrintGroup");
//将t1、t2线程放进这个线程组
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
}
守护线程(Daemon)
守护线程会随着所有非守护线程结束而结束
如果设置了 t 为主线程的守护线程,那么主线程结束之后,t 线程就会自动的结束
比如垃圾回收线程,JIT线程就是守护线程
public class DeamonDemo {
public static class DeamonT extends Thread{
public void run() {
while (true){
System.out.println("I am alive");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = new DeamonT();
t.setDaemon(true);
t.start();
Thread.sleep(2000);
}
}
这里需要注意的是,必须要在线程start之前设置为守护线程,不然会报错
线程优先级
线程可以设置1到10的优先级,高优先级在竞争资源时会有优势,但这里仅仅的概率问题,低优先级也有可能竞争成功
Thread.setPriority()就可以调用
synchronized
synchronized关键字是将需要同步的代码加锁,使得每次只能有一个进程进入同步快,从而保证线程间的安全性
synchronized有三种常用的方法
- 指定加锁的对象:对给定对象加锁,进入同步代码前要获得给定对想的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
- 直接作用于静态方法:相当于当前类加锁,进入同步代码前要获得当前类的锁
给对象加锁
public class AccountingSync implements Runnable{
static AccountingSync instance = new AccountingSync();
static int i = 0;
@Override
public void run() {
for(int j = 0; j < 10000000; j++) {
synchronized (instance) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果:
20000000