多线程的脉络梳理

一、什么是线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程与线程的区别

1.进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
4. 线程的创建、切换及终止效率更高。

二、为什么需要线程

1.创建一个新线程的代价要比创建一个新进程小得多
2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3. 线程占用的资源要比进程少很多
4. 能充分利用多处理器的可并行数量
5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

三、线程的基本知识

1.创建一个线程

方法一:继承Thread类
可以通过继承 Thread 来创建一个线程类

class MyThread extends Thread { 
	@Override 
	public void run() {
		System.out.println("这里是线程运行的代码"); 
	} 
}
MyThread t = new MyThread(); 
t.start(); // 线程开始运行

方法二:实现Runnable接口
通过实现 Runnable 接口,并且调用 Thread 的构造方法时将 Runnable 对象作为 target 参数传入来创建线程对象。

class MyRunnable implements Runnable {
	@Override 
	public void run() { 
		System.out.println("这里是线程运行的代码"); 
	} 
}
Thread t = new Thread(new MyRunnable()); 
t.start(); // 线程开始运行

2.Thread类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
线程中常见的构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable对象创建线程对象,并命名

Thread的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

• ID 是线程的唯一标识,不同线程不会重复
• 名称是各种调试工具用到
• 状态表示线程当前所处的一个情况
• 优先级高的线程理论上来说更容易被调度到
• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
• 是否存活,即简单的理解,为 run 方法是否运行结束了
• 线程的中断问题
启动一个线程
线程对象被创建出来并不意味着线程就开始运行
启动线程必须要调用 start 方法。
中断一个线程
目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;

        @Override
        public void run() {
            while (!isQuit) {
                System.out.println("子线程运行中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("子线程该停止了");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target);
        System.out.println("子线程开始运行");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println("让子线程停止");
        target.isQuit = true;
    }
}
  1. 调用 interrupt() 方法来通知
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (!Thread.interrupted()) {
                System.out.println("子线程正在运行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("子线程该停止了");
                    break;
                }
            }
        }

    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target);
        System.out.println("子线程开始运行");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println("让子线程停止");
        thread.interrupt();
    }
}

第二种方法中:

  1. 通过 thread 对象调用 interrupt() 方法通知该线程停止运行
  2. thread 收到通知的方式有两种:
    1.如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
    2.否则,只是内部的一个中断标志被设置,thread 可以通过
    (1)Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
    (2)Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
等待一个线程
通过调用join()方法来等待当前线程结束再执行后续操作

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("子线程还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("子线程结束了!");
        };
        Thread thread = new Thread(target);
        System.out.println("子线程开始工作");
        thread.start();
        thread.join();
        System.out.println("子线程工作结束了");
    }
}

2.状态的转移

观察线程的所有状态

public class ThreadState { 
	public static void main(String[] args) { 
		for (Thread.State state : Thread.State.values()) { 
			System.out.println(state); 
		} 
	} 
}
NEW 
RUNNABLE 
BLOCKED 
WAITING 
TIMED_WAITING 
TERMINATED

线程状态和状态转移
在这里插入图片描述
NEW : 初始化
RUNNABLE :RUNNING 正在运行;READY 等待系统调度
BLOCKED、WAITING、TIMED_WAITING :等待直到一些事情发生
TERMINATED:执行完成

wait()方法
其实wait()方法就是使线程停止运行。

  1. 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
  2. wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
  3. wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
public static void main(String[] args) throws InterruptedException { 
	Object object = new Object(); 
	synchronized (object) { 
		System.out.println("等待中..."); 
		object.wait(); 
		System.out.println("等待已过..."); 
	}
	System.out.println("main方法结束..."); 
}

notify()方法
notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
class MyThread implements Runnable {
    private boolean flag;
    private Object obj;

    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }

    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法开始.. " + Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法结束.. " + Thread.currentThread().getName());
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notify()方法开始.. " + Thread.currentThread().getName());
                obj.notify();
                System.out.println("notify()方法结束.. " + Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }
    }
}
public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread, "wait线程");
        Thread thread2 = new Thread(notifyThread, "notify线程");
        thread1.start();
        Thread.sleep(1000);
        thread2.start();
        System.out.println("main方法结束!!");
    }
}

从结果上来看第一个线程执行的是一个waitMethod方法,该方法里面有个死循环并且使用了wait方法进入等待状态将释放锁,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是notifyMethod方法,该方法里面执行了一个唤醒线程的操作,并且一直将notify的同步代码块执行完毕之后才会释放锁然后继续执行wait结束打印语句。
notifyAll()方法
notifyAll方法可以一次唤醒所有的等待线程

class MyThread implements Runnable {
    private boolean flag;
    private Object obj;

    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }

    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法开始.. " + Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法结束.. " + Thread.currentThread().getName());
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notifyAll()方法开始.. " + Thread.currentThread().getName());
                obj.notifyAll();
                System.out.println("notifyAll()方法结束.. " + Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }
    }
}

public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread1 = new MyThread(true, object);
        MyThread waitThread2 = new MyThread(true, object);
        MyThread waitThread3 = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread1, "wait线程A");
        Thread thread2 = new Thread(waitThread2, "wait线程B");
        Thread thread3 = new Thread(waitThread3, "wait线程C");
        Thread thread4 = new Thread(notifyThread, "notify线程");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        thread4.start();
        System.out.println("main方法结束!!");
    }
}

wait 和 sleep 的对比

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法

*yield()只是让出CPU,并不会改变自己的状态

四、线程共享资源

线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。

【个性】进程拥有这许多共性的同时,还拥有自己的个性,才能实现并发性

  1. 线程ID:每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
  2. 寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
  3. 线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。
  4. 错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该 线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
  5. 线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都 共享同样的信号处理器。
  6. 线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

五、线程安全问题

1.什么是线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

2.线程不安全的原因

原子性
指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
一条 java 语句不一定是原子的,也不一定只是一条指令,比如i++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
内存可见性
在这里插入图片描述
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
代码重排序
一段代码是这样的:
1.去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

代码重排序会给多线程带来什么问题?
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。

3.怎么判断代码是否有线程安全风险

1.是否是多线程环境?
单线程不会涉及线程安全问题。
2.是否有共享数据?
没有共享数据的多线程不会涉及线程安全问题
3.是否有多条语句操作共享数据?
没有多条语句操作共享数据不会涉及线程安全问题

4.通过什么机制保护线程安全

synchronized关键字

class MyThread implements Runnable { 
	private int ticket = 1000 ; // 一共十张票 
	@Override public void run() { 
		for (int i = 0; i < 1000; i++) { 
		// 在同一时刻,只允许一个线程进入代码块处理 
			synchronized(this) { // 表示为程序逻辑上锁 
				if(this.ticket>0) { // 还有票 
					try {
						Thread.sleep(20); 
					} catch (InterruptedException e) {
					}
					System.out.println(Thread.currentThread().getName()+",还有" +this.ticket -- +" 张票"); 
				} 
			} 
		} 
	} 
}
public class TestDemo { 
	public static void main(String[] args) { 
		MyThread mt = new MyThread(); 
		Thread t1 = new Thread(mt,"黄牛A"); 
		Thread t2 = new Thread(mt,"黄牛B"); 
		Thread t3 = new Thread(mt,"黄牛C"); 
		t1.start(); 
		t2.start(); 
		t3.start(); 
	} 
}

volatile 关键字
修饰的共享变量,可以保证可见性,部分保证顺序性

class ThraedDemo { 
	private volatile int n; 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值