Java高并发编程一--什么是线程
2018-02-18 00:00
253 查看
摘要: qq:853089986@qq.com
1.线程:进程中负责程序执行的执行单元
线程本身依靠程序进行运行
线程是程序中的顺序控制流,只能使用分配给程序的资源和环境
2.进程:执行中的程序
一个进程至少包含一个线程
3.单线程:程序中只存在一个线程,实际上主方法就是一个主线程
4.多线程:在一个程序中运行多个任务
目的是更好地使用CPU资源
我这里通俗一点讲其实就是:一个程序里头不同的执行路径,可以放在不同的cpu里面同步运行
就绪(runnable)状态: 调用了
运行(running)状态: 执行
阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
终止(dead)状态: 线程销毁
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。 当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。 线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。 当由于突然中断或者子任务执行完毕,线程就会被消亡。
在有些教程上将blocked、waiting、time waiting统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和Java中的方法调用联系起来,所以将waiting和time waiting两个状态分离出来。
注:sleep和wait的区别:
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
停止一个线程可以使用Thread.stop()方法,但最好不用它。该方法是不安全的,已被弃用。
在Java中有以下3种方法可以终止正在运行的线程:
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了
码云地址:https://gitee.com/zhangzeli/java-concurrent
1.什么是线程
在讲解java高并发时必须要先聊聊线程,那么什么是线程呢?下面是网上的答案:1.线程:进程中负责程序执行的执行单元
线程本身依靠程序进行运行
线程是程序中的顺序控制流,只能使用分配给程序的资源和环境
2.进程:执行中的程序
一个进程至少包含一个线程
3.单线程:程序中只存在一个线程,实际上主方法就是一个主线程
4.多线程:在一个程序中运行多个任务
目的是更好地使用CPU资源
我这里通俗一点讲其实就是:一个程序里头不同的执行路径,可以放在不同的cpu里面同步运行
2.如何启动一个线程
2.1继承Thread
//在java.lang包中定义, 继承Thread类必须重写run()方法 class MyThread extends Thread{ private static int num = 0; public MyThread(){ num++; } @Override public void run() { System.out.println("主动创建的第"+num+"个线程"); } } /*创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。 注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法, 即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别, 此时并不会创建一个新的线程来执行定义的任务。*/ public class Test { public static void main(String[] args) { System.out.println("主线程ID:"+Thread.currentThread().getId()); MyThread thread1 = new MyThread("thread1"); thread1.start(); MyThread thread2 = new MyThread("thread2"); thread2.run(); } } class MyThread extends Thread{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId()); }
2.2实现Runnable接口
//在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。 //实现Runnable接口必须重写其run方法。 //下面是一个例子: public class Test { public static void main(String[] args) { System.out.println("主线程ID:"+Thread.currentThread().getId()); MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } } class MyRunnable implements Runnable{ public MyRunnable() { } @Override public void run() { System.out.println("子线程ID:"+Thread.currentThread().getId()); } } //Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务, //然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数, //然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话, //是不会创建新线程的,这根普通的方法调用没有任何区别。 //事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。 //在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。 //直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承, //所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。
2.3使用ExecutorService、Callable、Future实现有返回结果的多线程
多线程后续会讲到,这里暂时先知道一下有这种方法即可。/** * 有返回值的线程 */ @SuppressWarnings("unchecked") public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println("----程序开始运行----"); Date date1 = new Date(); int taskSize = 5; // 创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(taskSize); // 创建多个有返回值的任务 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new MyCallable(i + " "); // 执行任务并获取Future对象 Future f = pool.submit(c); // System.out.println(">>>" + f.get().toString()); list.add(f); } // 关闭线程池 pool.shutdown(); // 获取所有并发任务的运行结果 for (Future f : list) { // 从Future对象上获取任务的返回值,并输出到控制台 System.out.println(">>>" + f.get().toString()); } Date date2 = new Date(); System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】"); } } class MyCallable implements Callable<Object> { private String taskNum; MyCallable(String taskNum) { this.taskNum = taskNum; } public Object call() throws Exception { System.out.println(">>>" + taskNum + "任务启动"); Date dateTmp1 = new Date(); Thread.sleep(1000); Date dateTmp2 = new Date(); long time = dateTmp2.getTime() - dateTmp1.getTime(); System.out.println(">>>" + taskNum + "任务终止"); return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"; } } /*代码说明: 上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。 public static ExecutorService newFixedThreadPool(int nThreads) 创建固定数目线程的线程池。 public static ExecutorService newCachedThreadPool() 创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。 如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。 public static ExecutorService newSingleThreadExecutor() 创建一个单线程化的Executor。 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。 ExecutoreService提供了submit()方法,传递一个Callable,或Runnable, 返回Future。如果Executor后台线程池还没有完成Callable的计算, 这调用返回Future对象的get()方法,会阻塞直到计算完成。*/
3.线程的状态
创建(new)状态: 准备好了一个多线程的对象就绪(runnable)状态: 调用了
start()方法, 等待CPU进行调度
运行(running)状态: 执行
run()方法
阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
终止(dead)状态: 线程销毁
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。 当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。 线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。 当由于突然中断或者子任务执行完毕,线程就会被消亡。
在有些教程上将blocked、waiting、time waiting统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和Java中的方法调用联系起来,所以将waiting和time waiting两个状态分离出来。
注:sleep和wait的区别:
sleep是
Thread类的方法,
wait是
Object类中定义的方法.
Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么
Thread.sleep不会让线程释放锁.
Thread.sleep和
Object.wait都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用
wait后, 需要别的线程执行
notify/notifyAll才能够重新获得CPU执行时间.
4.上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
5.线程的常用方法
编号 | 方法 | 说明 |
---|---|---|
1 | public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) | 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() | 中断线程。 |
8 | public final boolean isAlive() | 测试线程是否处于活动状态。 |
9 | public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
10 | public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
11 | public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
6.停止线程
停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。停止一个线程可以使用Thread.stop()方法,但最好不用它。该方法是不安全的,已被弃用。
在Java中有以下3种方法可以终止正在运行的线程:
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
暂停线程
interrupt()方法7.线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:
public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } } //在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10, //则JDK抛出异常throw new IllegalArgumentException()。 //JDK中使用3个常量来预置定义优先级的值,代码如下: public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; /*线程优先级特性: 继承性 比如A线程启动B线程,则B线程的优先级与A是一样的。 规则性 高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。 随机性 优先级较高的线程不一定每一次都先执行完。 */
8.守护线程
在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了
码云地址:https://gitee.com/zhangzeli/java-concurrent
相关文章推荐
- Java并发编程:进程和线程
- 【Java并发编程】之十一:线程间通信中notify通知的遗漏(含代码)(r)
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
- JAVA 并发编程随笔【五】Thread线程创建及运行线程任务
- JAVA 并发编程随笔【五】Thread线程创建及运行线程任务
- JAVA 并发编程随笔【六】线程的竞态条件与临界区
- JAVA 并发编程随笔【六】线程的竞态条件与临界区
- JAVA 并发编程-线程创建(二)
- Java并发编程:进程和线程之由来
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
- Java并发编程之线程管理(高级线程同步9)
- 【Java并发编程】之二:线程中断(含代码)
- 【Java并发编程实战】-----线程基本概念
- 【Java并发编程】之三:线程挂起、恢复与终止的正确方法(含代码)
- java并发编程实践 part 01 --> 线程创建方式
- 【Java并发编程】之十二:线程间通信中notifyAll造成的早期通知问题(含代码)
- 【Java并发编程】之三:线程挂起、恢复与终止的正确方法(含代码)
- [Java并发编程]-二、线程信息获取与设置
- JAVA 并发编程-多个线程之间共享数据(六)
- Java并发编程之十四:线程间通信中notify通知的遗漏(含代码)