线程基础
线程和进程
进程是操作系统的基础,是一个程序在一个数据集上运行的过程,也是系统进行资源分配和调度的基本单位。我们可以认为一个进程就是一个应用程序。
线程是操作系统调度的最小单元,程序执行的最小单位,在一个进程中可以创建多个线程。线程拥有独立的堆栈空间,可以共享内存变量。
为什么使用多线程
- 可以减少程序的响应时间
- 和进程相比线程的创建和销毁开销更小,进程之间是相互隔离的,线程之间可以共享内存变量,效率更高。
- 对于多cpu、多核的计算机而言,这些cpu可以同时执行不同的线程如果使用单线程,会使得资源不能很好的被利用造成浪费。
- 使用多线程可以简化程序的机构,让程序易于维护
线程的状态
Java的线程在运行的生命周期中可能会处理6种不同的状态
- 新建(New) 新建状态,线程刚被创建还没有调用start方法,在线程运行之前需要做一些基础工作
- 运行(Runnable)运行状态,一旦调用了start方法线程就处于Runnable状态,Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。所以说一个可运行的线程可能没有运行。
- 阻塞(Blocked)表示线程被阻塞,不能活动
- 等待(Waiting)等待状态,线程暂时不活动,并且不运行任何代码,直到线程调度器在重新激活它
- 超时等待(Timed waiting)和等待不同的是,它可以在指定时间自行返回
- 终止(Terminated)当前线程已经执行完毕。线程终止的原因有两个,第一种run方法执行完毕正常退出,第二种一个没有捕获的异常导致线程的终止
线程的执行流程:
线程创建后,调用Thread的start方法开始进入运行状态,执行wait方法后,进入等待状态,这个时候需要其他线程的通知才能重新返回运行状态,超时等待相当于在等待上加了一个时间,当时间到的时候,线程自动返回运行状态,当线程调用到同步方法时,如果没有获得锁,则进入阻塞状态,获取到锁后会进入到运行状态,当线程执行完或者遇到意外的时候都会进入到终止状态。
创建线程
- 继承Thread类,重写run方法。Thread本质也是实现了一个Runnable,调用start后并不会立即执行多线程的代码,而是当该线程变成可运行状态。而什么时候执行时由系统决定的。
//定义Thread子类
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
2.实现Runnable接口并实现run方法
public class RunnableThreadTest implements Runnable
{
public void run()
{
//集体逻辑
}
public static void main(String[] args)
{
RunnableThreadTest runnble = new RunnableThreadTest();
new Thread(runnble,"新线程").start();
}
}
3.实现Callable接口
Callable接口与Runnable接口功能类似,不过提供了更强大的功能。主要有以下几点
- Callable可以在任务技术后提供一个返回值
- Callable的call方法可以抛出异常,Runnable中的run方法不能
- 运行Callable可以拿到一个Future对象,Future对象是一步计算的结果,可以通过get()方法获得返回值,不过在执行get()方法的时候当前线程是阻塞的
public class CallableTest{
//创建callable线程
public static class MyCallable implements Callable{
public String call() throws Execption{
return "hello";
}
}
public static void main(String args[]){
MyCallable callable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadPool();
Future future = executorService.submit(callable);
try{
//等待线程结果
System.out.println(future.get());
}catch(Execption e){
e.printStackTrace
}
}
}
上面的三种方法一般推荐使用Runnable接口的方式。
线程的中断
一个线程可以调用其interrupt方法来中断线程。线程被中断了并不一定是被终止了,被终止是run方法执行完毕或者run方法中发生了异常而导致的。线程中断了还可以通过Thread.interrupted()方法进行复位。
想要安全的终止一个线程,可以在执行逻辑之前判断当前线程是不是中断的状态通过Thread.currentThread().idInterrupted()。或者通过一个boolean变量来判断,在Runnable中定义一个boolean变量使用volatile变量来修饰,vloatile可以保证这个变量的原子性,当别的线程修改这个变量的时候,所有的线程都会感受到这个变量的变化。
线程同步
在多线程应用中,如果两个或两个以上的线程同时对同一个对象进行修改的时候,就会产生问题,比如我仓库有一部手机,两个人在同一时间都下了订单并且都成功了,那手机给谁就不知道了。解决办法就是,当一个线程对此对象进行修改的时候,就给它一把锁,别的线程进不来,当它完成任务之后,在把锁给下一个进来的线程。在java中这个锁之一就是synchronized关键字。
使用synchronized可以给一个方法加上锁,wait()方法可以将一个线程添加到等待集中,notify或者notifyAll可以解除等待线程的阻塞状态。例如
public synchronized void add()throws InterruptedException{
//等待
for(int i = 0; i < 10;i++){
if(i == 3){
wait();
System.out.println("wait");
}
}
//进行一系列的操作
....
//唤醒
notifyAll();
System.out.println("notifyAll");
}
也可以给一个对象上锁
public class MyRunnable implements Runnable {
private Object flag;
private String threadName;
public MyThread(Object flag,String threadName) {
this.flag = flag;
this.threadName = threadName;
}
@Override
public void run(){
try{
for(int i = 0; i < 10;i++){
if(i == 3){
synchronized (this.flag){
//进入等待状态
this.flag.wait();
}
}
System.out.println(this.threadName + " " + i);
Thread.sleep(1000);
}
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
public class TestMain {
public static void main(String[] args) {
Object object = new Object();
MyRunnable myRunnable = new MyRunnable(object,"Runnable");
Thread thread = new Thread(myRunnable1);
thread.start();
try{
Thread.sleep(6000);
System.out.println("6秒后唤醒线程");
synchronized (object){
//唤醒线程
object.notify();
}
System.in.read();
} catch(InterruptedException e){
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
}
}
}
synchronized, wait() ,notify() 必须是操控的同一个对象
除了synchronized,java还提供了一个重用锁ReentrantLock。比synchronized写起来麻烦,不过功能更加强大。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
....
} finally {
lock.unlock(); // 看这里就可以
}
把unlock放在finally中是很有必要的,如果发生了异常确保锁是可以被释放的。synchronized中有wait(),notigy()等方法,在ReentrantLock需要配合Conditond 使用,Conditon提供了以下方法:
public interface Condition {
void await() throws InterruptedException; // 类似于Object.wait()
void awaitUninterruptibly(); // 与await()相同,但不会再等待过程中响应中断
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal(); // 类似于Obejct.notify()
void signalAll();
}
例子
public class Test {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
public class MyThread extends Thread {
private Test test;
public ThreadA(Test test) {
super();
this.test = test;
}
@Override
public void run() {
//等待
test.await();
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
MyThread a = new MyThread(test);
a.start();
Thread.sleep(3000);
//唤醒
service.signal();
}
volatitle
有时候如果只是给一个实例域使用同步的话,使用synchronize开销有点大,这个时候使用volatitle关键字修饰一个实例也可以做到线程安全。
当一个变量被volatitle关键字修饰以后,它就具备了两种含义:
- 第一是当一个线程修改了这个变量的值时,其他线程是可见的
- 第二个是禁止使用指令重排序(重排序是编译或者运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段)
Java的内存模型
Java中使用堆内存来存储对象的实例,堆内存是可以被所有线程共享的内存区域,因此它是内存可见的。
局部变量和方法定义的参数是不会再线程之间共享的,他们之间是内存不可见的,不受内存模型的影响。
Java的内存模型定义了线程和主存之间的抽象关系,线程之间的共享变量存储在主存当中,每个线程还有一个私有的本地内存(这是一个抽象的概念,不真实存在),本地内存中存储了该线程共享变量的副本,Java线程模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时对其他线程可见。
例如线程A和线程B之间要通信的话,需要经历两个步骤
- 线程A把本地内存中更新过的共享变量刷到主存中去
- 线程B到主存中读取A之前更新过的共享变量。
比如 int i = 5
执行此语句的线程,必须先在自己的工作线程中对变量i所在的缓存进行赋值操作,然后在写入到主存当中,而不是直接把3写入到主存中。
原子性 、 可见性 、有序性
- 原子性 原子性是指对这些操作是不能被中断的,要么执行完毕要么就不执行,比如对基本类型变量的读取和赋值都是原子性的。
x=3;是原子性的,
y=x;就不是原子性的了,因为其中执行了两部,先去内存中读取x的值,然后把x的值赋值给y
x++;也不是原子性的,它先读取x的值,然后对其加一,然后在把新值写入内存中。
从中我们可以看到,一个语句含有多个操作的时候,就不是原子性的。 - 可见性 线程之间的可见性是指一个线程修其状态对其他线程是可见的,一个线程的修改结果,另一个线程立刻就能看到。当一个共享变量使用volatitle关键字修饰的时候,就会保证修改的值能立马被更新到主存中,对其他线程可见。
普通的变量就做不到这点,因为普通的变量修改之后,并不会立即写入到主存当中,什么时候写入也是不确定的。当一个线程来读取该值得时候,主存中可能还是以前的值 - 有序性 Java内存模型中允许编译器和处理器对指令进行重排序(就是我们写的代码的顺序编译之后可能会变),重排序会对多线程并发产生一定的影响造成错误。我们可以通过volatitle关键字来保证有序性。
volatitle关键字可以保证变量的可见性和有序性,但是不能保证变量的原子性。所以一般来说使用volatitle的时候需要具备以下条件
1 对变量的操作不依赖于当前的值
2 该变量没有包含在具有其他变量的不变式中
volatile关键字的一般使用场景
1 状态标记变量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
2 写单例的时候 双重校验
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}