并发编程(一)多线程基础
一、基础概念
多线程的学习从一些概念开始,进程和线程,并发与并行,同步与异步,高并发。
几乎所有的操作系统都支持同时运行期多个任务,所有运行中的任务通常就是一个进程,进程是处于运行过程中的程序,进程是操作系统进行资源分配和调度的一个独立单位。
进程有三个如下特征:
-
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
-
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中部是不具备的。
-
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
线程是进程的组成部分,一个进程可以拥有多个线程,而线程必须有一个父进程,线程可以有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源。比如使用QQ时,我们可以同事传文件,发送图片,聊天,这就是多个线程在进行。
线程可以完成一定的任务,线程能够独立运行的,它不知道有其他线程的存在,线程的执行是抢占式的,当前线程随时可能被挂起。
总之:一个程序运行后至少有一个进程,一个进程里可以有多个线程,但至少要有一个线程。
1.2 并发和并行
并发和并行是比较容易混淆的概念,他们都表示两个或者多个任务一起执行,但并发侧重多个任务交替执行,同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。而并行确实真正的同时执行,有多条指令在多个处理器上同时执行,并行的前提条件就是多核CPU。
1.3 同步和异步
同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
1.4 高并发
高并发一般是指在短时间内遇到大量操作请求,非常具有代表性的场景是秒杀活动与抢票,高并发是互联网分布式系统架构设计中必须考虑的因素之一,高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式
1.5 多线程的好处
线程在程序中是独立的、并发的执行流,拥有独立的内存单元,多个线程共享父进程里的全部资源,线程共享的环境有进程的代码段,进程的公有数据等,利用这些共享数据,线程很容易实现相互之间的通信,可以提高程序的运行效率。
多线程的好处主要有:
-
进程之间不能共享内存,但线程之间共享内存非常容易。
-
系统创建进程时需要给进程重新分配系统资源,但创建线程代价小得多,所以使用多线程实现多任务并发比多进程效率高
-
Java语言内置了多线程功能支持。
二、创建多线程
上面讲了多线程的一些概念,都有些抽象,下面将学习如何使用多线程,创建多线程的方式有三种。
2.1 继承Thread类创建
继承Thread创建并启动多线程有三个步骤:
-
定义类并继承Thread,重写run()方法,run()方法中为需要多线程执行的任务。
-
创建该类的实例,即创建了线程对象。
-
调用实例的start()方法启动线程。
public class FirstThread extends Thread { private int i=0; public void run() { for (; i < 100; i++) { //获取当前线程名称 System.out.println(this.getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 new FirstThread().start(); new FirstThread().start(); } } } }
运行结果可以看到两个线程的i并不是连续的,说明他们并不共享数据。
2.2 实现Runnable接口
实现Runnable接口创建并启动多线程也有以下步骤:
-
定义类并继承Runnable接口,重写run()方法,run()方法中为需要多线程执行的任务。
-
创建该类的实例,并以此实例作为target为参数来创建Thread对象,这个Thread对象才是真正的多线程对象。
public class SecondThread implements Runnable { private int i = 0; @Override public void run() { for (; i < 100; i++) { //此时想要获取到多线程对象,只能使用Thread.currentThread()方法 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 SecondThread secondThread=new SecondThread(); new Thread(secondThread,"线程一").start(); new Thread(secondThread,"线程二").start(); } } } }
2.3 使用Callable和Future
Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,并且可以申明抛出异常,使用Callable创建的步骤如下:
-
定义类并继承Callable接口,重写call()方法,run()方法中为需要多线程执行的任务。
-
创建类实例,使用FutureTask来包装对象实例,
-
使用FutureTask对象作为Thread的target来创建多线程,并启动线程。
-
调用FutureTask对象的get()方法来获取子线程结束后的返回值。
public class ThirdThread { public static void main(String[] args) { //使用lambda表达式 FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> { int i = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i); } return i; }); for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 new Thread(task, "有返回值的线程").start(); } } try { System.out.println("线程的返回值:" + task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
这里使用了lambda表达式,不使用表达式的方式也很简单,可以去源码中查看。Callable与Runnable方式基本相同,只不过增加了返回值且可允许声明抛出异常。
使用三种方式都可以创建线程,且方式也相对简单,大体分为实现接口和实现Thread类两种,这两种都各有优缺点。
继承接口实现:
- 优点:除了继承接口之外,还可以继承其他类。这种方式多个线程共享一个target对象,可以处理用于共同资源的情况。
- 缺点:编程稍微复杂一些,并且没有直接获取当前线程对象的方式,必须使用Thread.currentThread()方式。
基础Thread类:
-
优点:编程简单
-
缺点:不能继承其他类
三、操作多线程
3.1 多线程的状态
线程状态是线程中非常重要的一个概念,然而我看过很多资料,线程的状态理解有很多种方式,很多人将其分为五个基本状态:新建、就绪、运行、阻塞、死亡,但在状态枚举中并不是这五个状态,我不知道是什么原因(有大神可以解答更好),只能按照枚举中的状态根据自己的理解。
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法,而且就算调用了改方法也不代表状态立即改变。
-
运行(RUNNABLE):在运行的状态肯定就处于RUNNABLE状态。
-
阻塞(BLOCKED):表示线程阻塞,或者说线程已经被挂起了。
-
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
-
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
状态流程图如下:
理解:初始状态很好理解,这个时候其实还不能被称为一个线程,因为他还没被启动,当调用start()方法后,线程正式启动,但是也不代表立即就改变了状态。
运行状态中其实包含两种状态,运行中(RUNING)和就绪(READY)。
就绪状态表示你有资格运行,只要CPU还未调度到你,就处于就绪状态,有几个状态会是线程状态编程就绪状态
-
调用线程的start()方法。
-
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁。
-
当前线程时间片用完了,调用当前线程的yield()方法。
-
锁池里的线程拿到对象锁后。
运行中(RUNING)状态比较好理解,线程调度程序选择了当前线程作。
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待状态是指线程没有被CPU分配执行时间,需要等待,这种等待是需要被显示的唤醒,否则会无限等待下去。
超时等待状态是这现在没有被CPU分配执行时间,需要等待,不过这种等待不需要被显示的唤醒,会设置一定的时间后zi懂唤醒。
死亡状态也很好理解,说明线程方法被执行完成,或者出错了,线程一旦进入这个状态就代表彻底的结束
如何中断线程是多线程开发的重要技术点,说明中断线程不像break语句那样简单干脆,中断线程处理不好的话会出现难以定位的错误,中断线程表示要求线程在完成任务之前停止正在做的操作,很明显我们必须妥善的处理,否则会出现奇怪的错误,stop()方法可以停止线程,但最好不用它,因为他是不安全的,并且已经被废弃,正在被使用的是interrupted。
interrupted并不是马上停止线程,而是给线程打一个停止标记,将线程的中断状态设置为true,这类似老板让你好好工作,但是到底好不好工作要看你自己。
public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 50000; i++) { System.out.println("i=" + (i + 1)); } } public static void main(String[] args) { try { MyThread myThread = new MyThread(); myThread.start(); Thread.sleep(2000); myThread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果显示还是会打印5万行数据。
Thread提供了两个方法来判断线程是否终止
-
static boolean interrupted():判断当前线程是否中断,清除中断标志。
-
boolean isInterrupted():判断线程是否中断,不清除中断标志。
Thread.currentThread().interrupt(); System.out.println("是否停止1?=" + Thread.interrupted());//true,执行方法后清除了标记 System.out.println("是否停止2?=" + Thread.interrupted());//false
当调用了interrupted后线程未真正的停止,但已经有了标志状态,也就是说我们可以通过标志状态来对我们的多线程执行的方法进行处理。
public class FiveThread extends Thread { @Override public void run() { for (int i = 0; i < 500000; i++) { if (this.isInterrupted()) { System.out.println("已经是停止状态了!退出!"); break; } System.out.println("i=" + (i + 1)); } System.out.println("666"); } public static void main(String[] args) { try { FiveThread thread = new FiveThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } }
异常法停止线程
这样虽然可以实现退出for循环,但是在for循环之外的代码依然会被执行,很明显这样没有达到效果,这个时候我们可以抛出异常:
@Override public void run() { try { for (int i = 0; i < 500000; i++) { if (this.isInterrupted()) { System.out.println("已经是停止状态了!退出!"); throw new InterruptedException(); } System.out.println("i=" + (i + 1)); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("抛出了错误!"); } System.out.println("666"); }
当然我们也可以使用return方式进行处理,但还是抛出异常处理比较好,可以让线程中断事件得到传播。
public class JoinThread extends Thread { private Thread thread; public JoinThread(Thread thread) { this.thread = thread; } @Override public void run() { try { thread.join(); for (int i = 0; i < 10; i++) { System.out.println(thread.getName() + "的执行 " + i); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Thread previousThread = Thread.currentThread(); for (int i = 1; i <= 10; i++) { Thread curThread = new JoinThread(previousThread); curThread.start(); previousThread = curThread; } } }
在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程。
join方法有三个重载:
-
join():等待被join的线程执行完成。
-
join(long millis):等待被join的线程millis毫秒,如到时仍未执行完成,则不再等待。
-
join(long millis,int nanos):等待被join的线程millis毫秒加nanos微妙。
3.4 sleep
想让当前的线程暂停一段时间,并进入阻塞状态,就可以使用sleep方法,这是一个Thread的静态方法,使用也很简单。一旦调用了sleep方法,线程就不会获得执行的机会,即是没有其他线程执行,sleep方法不会失去锁。sleep方法经常会拿来Object.wait()方法进行比较。
两者主要的区别:
-
sleep()方法是Thread的静态方法,而wait是Object实例方法
-
wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
-
sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
3.5 yield
yield也是一个静态方法,一旦执行,它会让当前线程让出CPU,使线程进入就绪状态,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。
public class YieldThread extends Thread { public YieldThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 50; i++) { //获取当前线程名称 System.out.println(this.getName() + " " + i); if (i == 20) { Thread.yield(); } } } public static void main(String[] args) { YieldThread yieldThread = new YieldThread("高级"); //yieldThread.setPriority(Thread.MAX_PRIORITY); yieldThread.start(); YieldThread yieldThread2 = new YieldThread("低级"); //yieldThread2.setPriority(Thread.MIN_PRIORITY); yieldThread2.start(); } }
在未设置线程优先级时,第一个线程执行到20时会让给第二个线程运行,可是在设置了线程优先级后,高级线程在执行yield后发现没有比他优先级更高的线程,就又会开始执行,并不会中断。
四、线程优先级
每个线程执行时都有一定的优先级,优先级高的线程获得更多的执行机会,优先级低的线程则获得少的执行机会,线程的默认级别与创建它的父线程级别相同,main的级别为普通级别,那么由他创建的线程都是普通级别。
setPriority可以设置线程的优先级,范围在1-10之间,也可以使用三个静态常量:
-
MAX_PRIORITY:值为10
-
MIN_PRIORITY:值为1
-
NORM_PRIORITY:值为5
当线程的优先级既有它的规律性,即cpu尽量将资源给优先级高的,但是也有一定的随机性,优先级较高的不一定先执行完run方法。
五、守护线程
Java中有两种线程,一种是用户线程,一种是守护线程,守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者。
典型的守护线程是垃圾回收线程,当进程中没有非守护线程了,那守护线程就没有必要存在了,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。
public class DaemonThread extends Thread { @Override public void run() { while (true) { try { System.out.println("i am alive"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("finally block"); } } } public static void main(String[] args) { DaemonThread daemonThread = new DaemonThread(); daemonThread.setDaemon(true); daemonThread.start(); //确保main线程结束前能给daemonThread能够分到时间片 try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } }
这个例子中的线程如果不设置为守护线程,是一个死循环,会一直执行,当我们把它设置为守护线程后,在主线程执行完成后,守护线程也会退出,但是需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的。
线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法。