java线程基础知识总结

概念

进程与线程

  • 进程
    一段运行的程序,一个应用程序就是一个进程,如QQ、浏览器、网易云音乐,但进程不一定就是应用程序,有可能运行在后台,具体定义可以查看百度或者计算机操作系统相关的书籍。

  • 线程
    线程是独立调度的最小单位,一个进程中至少有一个线程,即主线程。当一个进程没有开启其他线程,即单线程,代码顺序执行,若开启了其他线程,则CPU会轮流调度每个线程,即每个线程的代码交替执行。
    比如微信在接收文件时,我们还可以输入文字并发送给对方,这里涉及3个线程,一个负责接收文件,一个负责响应UI操作(点击输入框打字),一个负责发送信息,这三个线程轮流占用CPU,让我们看起来是同时的。

并行与并发

  • 并行
    并行是指两件事情在同一时刻发生,比如两个CPU同时执行一段代码,是真正意义上的同时。

  • 并发
    并发是在一个时间段内发生两件事情,但有先后顺序,不是真正的同时,CPU调度线程的过程就是并发,只不过它们轮换CPU的速度很快,我们察觉不了,看起来是多个线程同时执行,所以多线程编程也叫并发编程。

同步和异步

  • 同步
    同步并不是两件事同时发生,而是多个事情的发生有确定的次序,线程同步是指线程的执行有明确的顺序,通常是通过加锁来实现。

  • 异步
    相对于同步,异步是指多个事件的发生次序不确定,线程如果不加控制就是异步的,但异步的无序性造成了代码运行的不确定性,所以要人为的控制线程,使线程同步。

线程的状态转换(生命周期)

在这里插入图片描述

新建(New)

线程也是一个对象,使用对象就要先实例化,刚实例化的线程就处于新建状态。

可运行状态(Runnable)

刚创建的线程调用Thread.start()后就变成可运行状态,这个状态包括了ready和running的线程,running是指正在占用CPU资源的线程,ready是等待被CPU调度的线程。

阻塞(Blocked)

线程在运行时如果被加上了锁(Synchronized或Lock),线程就会进入阻塞状态,其他线程释放掉锁,阻塞的线程才会重新回到可运行状态。

限时等待(Time Waiting)

运行中的线程调用Thread.sleep()或带参数的Object.wait()等方法会进入限时等待状态,进入等待状态通常被称为“线程睡眠”,等事件结束或其他线程显式调用Object.notify()或Object.notifyAll()方法,该状态的线程会被唤醒,即变回可运行状态。

进入方法退出方法
Thread.sleep()方法时间结束
设置了 Timeout 参数的 Object.wait() 方法时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法时间结束 / 被调用的线程执行完毕

无限等待(Waiting)

运行中的线程调用Object.wait()方法或调用了其他线程的join()方法,会使该线程进入无限等待状态,在这个状态中只能等待其他线程显式地唤醒,否则不能被CPU调度。

进入方法退出方法
没有设置 Timeout 参数的 Object.wait() 方法Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法被调用的线程执行完毕

死亡(Terminated)

当线程调用start()方法开启后,无论在哪个状态中,只要抛出异常,线程就会终结,即进入死亡状态。当然,线程正常执行完任务后也会终结。

线程的使用

继承Thread类

继承Thread类并重写run()方法。

public class MyThread extends Thread {
    public void run() {
        System.out.println("继承Thread类的线程运行!");
    }
}
public static void main(String[] args) {
    MyThread myThread = new MyThread();
    myThread.start();
}

实现Runnable接口

实现Runnable接口,给出run()方法的实现,实例化Runnable对象传递给Thread,调用start()方法。

public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("实现Runnable接口的线程运行!");
    }
}
public static void main(String[] args) {
    Thread myThread = new Thread(new MyRunnable());
    myThread.start();
}

实现Callable接口

相对于Runnable,Callable可以有返回值,返回值通过通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

日常开发中,前两种方式用得比较多。无论是哪种方式,最后都要通过调用Thread.start()来启动线程。

实现接口 VS 继承Thread类

推荐通过实现接口来开启线程,因为java不支持多继承,如果继承Thread类就无法继承其他类,但接口可以实现多个。

线程的常用方法

Thread.sleep()

这个方法接收一个long型的参数,表示毫秒,比如Thread.sleep(3000)代表调用该方法的线程进入等待状态,3秒后再进入可运行状态。看一下例子(因为只装了Android Studio,就用AS来测试了)

private class MyTask1 implements Runnable {
        @Override
        public void run() {
            try {
                Log.d(getClass().getName(), "run: 睡眠3秒");
                Thread.sleep(3000);
                Log.d(getClass().getName(), "run: 睡眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_sleep);

        Thread thread = new Thread(new MyTask1());
        thread.start();
    }

在这里插入图片描述
可以看到两条日志的时间相差3秒(还有2ms可以忽略)。需要注意的是,Thread.sleep()方法有可能抛出异常,而异常又不能跨线程抛给其他线程,所以要用try-catch语句在本线程处理该异常。另外,这个方法还有一个接收两个参数的,public static void sleep(long millis, int nanos),第二个参数表示额外等待的纳秒时间,日常开发中用毫秒的就够了,除非对时间有特别严谨的要求。

Thread.yield()

yield的意思是放弃、让步,若某线程调用该方法,则表示该线程已完成某些重要的步骤,建议CPU调度其他线程执行。

public void run() {
    Thread.yield();
}

线程的中断

InterruptedException

通过调用指定线程的interrupt()方法,如果该线程处于阻塞、限时等待或无限等待状态,那么会抛出InterruptedException,从而提前结束该进程。看个例子。

private class MyTask1 implements Runnable {
        @Override
        public void run() {
            try {
                Log.d(getClass().getName(), "run: 线程1睡眠");
                Thread.sleep(5000);
                Log.d(getClass().getName(), "run: 线程1睡眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_interrupted);

        Thread threadA = new Thread(new MyTask1());
        threadA.start();
        threadA.interrupt();
    }

在这里插入图片描述可以看到线程1本来要睡眠5秒,但在主线程中调用了线程1的interrupt()方法,因为线程1在限时等待状态,所以抛出了异常,导致线程提前中断。

interrupt()

如果线程没有使用sleep()、join()、wait()等有可能会抛出InterruptedException异常的方法,那么调用线程的interrupt()不能让线程中断,但是interrupt()会让该线程设置一个中断标志,线程内部调用interrupted() 方法会返回 true,所以线程内部可以用循环判断该线程是否可中断,从而提前结束线程。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_interrupted);

        MyThread myThread = new MyThread();
        myThread.start();
        try {
        	// 主线程睡眠5ms再给子线程设置中断标记,让子线程有机会执行
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myThread.interrupt();
    }
private class MyThread extends Thread{
        @Override
        public void run() {
            while (!isInterrupted()) {
                Log.d(getClass().getName(), "run: 线程运行");
            }
            Log.d(getClass().getName(), "run: 跳出循环");
        }
    }

在这里插入图片描述
注意,这种方法只能通过继承Thread类使用,因为isInterrupted()是Thread类内部的方法。

线程同步

为什么要让线程同步

这个问题相当于为什么不能让线程异步,上面提到线程的异步具有不确定性,就是通常说的线程不安全。什么意思?举个例子。

public class ThreadNotSafeActivity extends AppCompatActivity {
    private volatile int count = 0;
    private volatile boolean isThreadADone = false;
    private volatile boolean isThreadBDone = false;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_not_safe);

        new Thread(new Runnable() {
            @Override
            
            public void run() {
                for (int i=1;i<=500000;i++){
                    count++;
                }
                isThreadADone = true;
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=1;i<=500000;i++){
                    count++;
                }
                isThreadBDone = true;

            }
        }).start();

        while(!isThreadADone || !isThreadBDone){
        
        }
        Log.d(getClass().getName(), "run: count最终值" + count);
    }
}

在这里插入图片描述
这段代码很简单,就是两个线程各自循环50万次,递增count变量,预期的结果应该是count最后等于1000000,而这次运行的结果903248,而且同一条件下再运行一次,结果又不一样了,这就是线程不安全的例子。如果放到银行的系统中,两个人同时存50万(虽然不可能每次存一块钱),但是银行收到的钱却不是100万,这是绝对不允许出现的情况。

为什么会出现这种情况?就上面的例子来看,count++不是一个原子操作。那什么是原子操作呢?(关于原子性、可见性、重排序的知识又是一个专题,之后再写总结,如果不懂的可以看一下别人关于这方面写的博客,最好是结合书本系统学习,比如《深入理解java虚拟机》和《java并发编程的艺术》)

通俗地说,原子操作是指线程在做这个操作时,其他线程不能打断。例如count++就包含了3个原子操作:
1)从内存读取count的值
2)count + 1
3)将count的值写回到内存中
这就是上面线程不安全的主要原因,比如出现一种情况,当前count的值为100,当线程A读取count的值并加1,在写回内存之前线程B也读取count的值(此时内存中count还是100),然后加1,写回内存,此时count等于101,但是线程A中此时也把count的值(101)写回内存,这样执行了两次加1操作,而count最终的值却是101,不符合预期的结果。

因此,对于有依赖关系的线程或访问同一变量的线程,我们要让这些线程同步。Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

synchronized

同步代码块

public void func() {
    synchronized (this) {
        // ...
    }
}

这种方法只作用在一个对象上,调用不同对象的同步代码块,不会进行同步。如例子所示。

private class MyClass {
        public void fun(){
            synchronized (this) {
                for (int i=1;i<=10;i++)
                    Log.d(getClass().getName(), "fun: " + i);
            }
        }
    }
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_synchronized_sample);

       final MyClass myClass = new MyClass();
       new Thread(new Runnable() {
            @Override
            public void run() {
                myClass.fun();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                myClass.fun();
            }
        }).start();

    }

在这里插入图片描述
上面的代码使两个线程同步,一个线程循环结束后,另一个线程才可以进入循环,但这只能同步一个对象,如果两个线程操作不同的对象则不会同步。把上面代码改一下:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_synchronized_sample);

        final MyClass myClass = new MyClass();
        final MyClass myClass1 = new MyClass();

        new Thread(new Runnable() {
            @Override
            public void run() {
                myClass.fun();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                myClass1.fun();
            }
        }).start();

    }

在这里插入图片描述
同步一个方法

public synchronized void func () {
    // ...
}

跟同步代码块一样,作用于一个对象。

同步一个类

public void func() {
    synchronized (MyClass.class) {
        // ...
    }
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

private class MyClass {
        public void funA(){
            synchronized (MyClass.class) {
                for (int i=1;i<=100;i++)
                    Log.d(getClass().getName(), "fun: " + i);
            }
        }
    }
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_synchronized_sample);

        final MyClass myClass = new MyClass();
        final MyClass myClass1 = new MyClass();

        new Thread(new Runnable() {
            @Override
            public void run() {
                myClass.funA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                myClass1.funA();
            }
        }).start();

    }

在这里插入图片描述
可以看到两个对象同步了,一个线程循环结束另一个线程才进入循环。

同步一个静态方法

public synchronized static void fun() {
    // ...
}

作用于整个类。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。用法如下:

public class ReentrantLockExampleActivity extends AppCompatActivity {
    private Lock lock = new ReentrantLock();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reentrant_lock_example);

        final MyClass myClass = new MyClass();
        final MyClass myClass1 = new MyClass();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    myClass.fun();
                }finally {
                    // 确保解锁,避免死锁
                    lock.unlock();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    myClass1.fun();
                }finally {
                    // 确保解锁,避免死锁
                    lock.unlock();
                }
            }
        }).start();
    }

    private class MyClass {
        public void fun() {
            for(int i=1;i<=100;i++)
                Log.d(getClass().getName(), "fun: "+ i);
        }
    }
}

在这里插入图片描述
lock.lock()的作用是尝试获得锁,如果获得锁,则进入同步语句块,若该锁被其他线程获取,则进入阻塞状态等待其他线程释放该锁;lock.unlock()的作用就是释放锁,让其他被阻塞的线程可以获得锁。

这里只是介绍了ReentrantLock的简单用法,还有很多更加高级的功能,大家可以查阅一下(以后我也会自己写一篇总结)。要注意的是lock和unlock要成对使用,避免死锁。

死锁
死锁是多线程编程中的一个重要概念,通俗地讲,就是线程之间要获取对方的资源才能释放自身占用的资源,这样就进入一个死循环,导致线程不能执行下去。锁能让线程同步,但也有可能造成死锁,所以我们要避免死锁的发生。改一下上面的代码:

public class ReentrantLockExampleActivity extends AppCompatActivity {
    private Lock lock = new ReentrantLock();
    private Lock lock1 = new ReentrantLock();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reentrant_lock_example);

        final MyClass myClass = new MyClass();
        final MyClass myClass1 = new MyClass();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lock();
                try {
                    myClass.fun();
                }finally {
                    // 确保解锁,避免死锁
                    lock1.unlock();
                    lock.unlock();

                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock1.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
                try {
                    myClass1.fun();
                }finally {
                    // 确保解锁,避免死锁
                    lock.unlock();
                    lock1.unlock();
                }
            }
        }).start();
    }

    private class MyClass {
        public void fun() {
            for(int i=1;i<=100;i++)
                Log.d(getClass().getName(), "fun: "+ i);
        }
    }
}

上面就是会发生死锁的例子,线程A获得锁lock后睡眠3秒,此时线程B获得锁lock1,也睡眠3秒,等睡眠结束时,线程A和线程B都尝试获取对方占有的锁,于是两个线程都进入阻塞,始终无法释放占有的资源,进入死锁,所以无论等多久,也不会有日志打印出来。

关于死锁的知识还有很多,如死锁的4个必要条件、避免死锁的方法等,这里只简单介绍死锁的概念。

synchronized VS ReentrantLock

1.锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2.性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3.等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
4.公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

线程协调

join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。

public class JoinExampleActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_join_example);

        final Thread threadA = new Thread(){
            @Override
            public void run() {
                Log.d(getClass().getName(), "run: 线程A执行");
            }
        };

        Thread threadB = new Thread(){
            @Override
            public void run() {
                Log.d(getClass().getName(), "run: 先让线程A执行");
                try {
                    threadA.join();
                    Log.d(getClass().getName(), "run: 线程B执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };

        threadB.start();
        threadA.start();
    }
}

在这里插入图片描述

wait()、notify()、notifyAll()

调用 wait() 使得线程等待某个条件满足(即进入无限等待状态),线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

这三个方法只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException(即要和synchronized搭配使用)。

它们都属于 Object 的一部分,而不属于 Thread。这些方法的作用是改变线程的状态,为什么不属于Thread而属于Object?
我的理解是,这些方法不仅可以改变线程状态,更重要的是这些方法也会对锁进行操作,比如调用wait()会释放synchronized,让其他阻塞的线程进入同步语句块(所以这些方法一定要在synchronized语句内使用),而synchronized就是在对象上加锁(用synchronized修饰方法或语句块其实就是给所属对象加锁),那解锁当然是解某个对象上的锁,所以这些方法放在Object里是合理的。

按照惯例,举个例子。

public class WaitNotifyExampleActivity extends AppCompatActivity {
    private Object object = new Object();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_wait_notify_example);

        Thread threadA = new Thread(){
            @Override
            public void run() {
                synchronized (object){
                    try {
                        object.wait();
                        Log.d(getClass().getName(), "run: 线程A运行");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread threadB = new Thread(){
            @Override
            public void run() {
                synchronized (object){
                    Log.d(getClass().getName(), "run: 线程B运行");
                    object.notify();
                }
            }
        };

        threadA.start();
        try {
        	// 主线程睡眠1秒再开启线程B
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadB.start();
    }
}

在这里插入图片描述
notify()随机唤醒一个线程,notifyAll()唤醒所有线程,前提是这些线程都在等同一把锁,等待其他锁的线程不会被唤醒。
wait()和sleep()的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

和Lock搭配使用,相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

public class AwaitSignalExampleActivity extends AppCompatActivity {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_await_signal_example);

        Thread threadA = new Thread(){
            @Override
            public void run() {
                lock.lock();
                try {
                    conditionA.await();
                    Log.d(getClass().getName(), "run: 线程A被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread threadB = new Thread(){
            @Override
            public void run() {
                lock.lock();
                try {
                    conditionB.await();
                    Log.d(getClass().getName(), "run: 线程B被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        };

        Thread threadC = new Thread(){
            @Override
            public void run() {
                lock.lock();
                Log.d(getClass().getName(), "run: 线程C运行");
                conditionA.signalAll();
                lock.unlock();
            }
        };

        threadA.start();
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadC.start();
    }
}

可以看到线程A和线程B开启后,就执行不同condition的await()方法,进入等待状态,主线程睡眠1秒后再开启线程C,在线程C中,调用conditionA的signalAll()方法,结果应该只有线程A被唤醒。
在这里插入图片描述
结果符合预期,可以推断出如果线程C中的conditionA.signalAll();改成conditionB.signalAll();,那么线程B会被唤醒,如果两句都有,则线程AB都会被激活。

wait() VS await()

  • wait()、notify()、notifyAll()搭配synchronized使用,它们都属于Object的方法,await()、signal()、signalAll()搭配Lock和Condition使用,是Condition中的方法。
  • notify()随机唤醒一个线程,signal()唤醒指定条件的线程,更加灵活。日常开发中,用synchronized和wait()、notify()、notifyAll()就可以应对大多数场景,且更安全(不会发生死锁),但如果想更精准的控制线程的调度,则使用另外一种。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值