Java高并发编程一--什么是线程

2018-02-27 11:51:20来源:oschina作者:张泽立人点击

分享
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 list = new ArrayList();
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 {
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

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台