一、 进程与线程的
1.1 进程与线程的区别及查看方法
- 进程是操作系统运行的一个程序单元,是系统管理的最基本单元,资源分配和调度的独立单位,单任务系统的缺点是存在任务等待时,CPU处于等待状态即空转,导致资源浪费,windows中可以在任务管理器查看每个进程,例如QQ.exe,linux中可以使用top或者ps查看。
-
线程是进程中独立运行的子任务,例如windows中每个任务下的子任务,如下图:
进程及线程示意图 - 在linux中各个语言均提供了查看的命令及方法,例如java中可以使用
jps
查看各个java
进程及其名称和pid
,利用jstack job_id
查看每个线程的运行状态。
1.2 多线程的优势及劣势
- 异步执行,降低CPU等待时空转的时间,通过多任务上下文切换充分利用CPU的资源,切换时每个线程会将独有的栈变量入栈,在下次获取到执行权时再次从栈中获取变量。
- 利用阻塞时的空闲CPU资源
线程数≈(运行时间+阻塞时间)/ 运行时间
,故线程数量并非越多越好,计算密集型任务线程数数量最好为N+1,IO密集型任务线程数量最好为2N+1
- 均分运行资源,让多个任务同时推进而非只服务于一个任务。
1.3 并发与并行的区别
并发与并行均表示任务的一起执行,并发偏重于逻辑上的同时执行,并行侧重于物理并行例如硬件FPGA,由于线程切换快,感觉上就是并行,但实际上并非如此。
二、java中的线程基本操作
2.1 java多线程创建入门
main
线程,垃圾回收线程(deamon
)为最常见的线程,使用Thread.currentThead()
获取当前线程,例如Thread.currentThead.getName()
,Java中线程的创建方式主要有以下三种:
-
继承Thead类重新run方法,然后执行start方法即可
,一般会将对象传入Thead构造,可以方便的对线程进行其他操作以及隔离非线程相关的操作方法
,例如设定优先级,命名等,Thead源码结构为public class Thread implements Runnable
,由于java属于单继承,故一般不建议采用该方法来创建线程。 - 实现
Runnable
接口,由Thead
原型可知,可以直接实现Runnaable
接口来创建线程,可以避免由于单继承导致无法继承其他类的弊端,Runnable
接口原型如下:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
实现类只需要实现run
方法即可,由于Runnable
接口同时也支持函数式编程,故也可以使用使用lambda表达式
来创建一个匿名线程,实现Runnable
接口后可以传入Thead构造方法
创建线程,也可执行start
- 实现
Callable<V>
接口,可以创建带返回值的线程,接口原型如下:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
实现该接口的V call() throws Exception
方法,该方法返回值是一个泛型,故需要在实现或者使用时指定泛型的类型,然后将Callable<V>
的实现类交由FutuerTask<V>
,封装成Thead的传入参数(实际还是Runnable),然后传入Thead再执行start方法
,FutuerTask<V>
及其继承的接口原型如下
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
public interface Future<V>
使用方法如下:
// lambda表达式创建Callable<V> throws Exception,并实现 V call()方法
Callable<Integer> callable = () -> {
System.out.println('a');
return 1;
};
// 封装成Runnable 供Thead创建线程
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread call = new Thread(futureTask,"futureTask");
call.start();
// 获取返回值
System.out.println(futureTask.get());
2.2 三种创建方式的比较
- 继承
Thead
来创建线程,有利于类方便的获取线程的各种状态,比如alive
、name
等,但缺点在于无法再继承其他类且无返回值。 - 实现
Runnable
接口创建线程,需要实现Runnable
接口,可以避免java的单继承问题,但只能通过Thead.currentThead
来获取线程的状态和属性,无返回值. - 实现
Callable<V>
接口再结合FutureTask<V>
既可以避免单继承问题,也可以有返回值FutureTask.get
或者超时版本get(long timeout, TimeUnit unit)
来拿到线程的执行结果,有利于异步计算结果汇总。
2.2 线程安全
- 在每个线程变量都独立时即没有共享变量,各个都能够独立且正确的完成工作,若存在共享变量则由于内存空间和每个线程的栈数据不一致的情况,这块是重点,后续再仔细分析。
2.3 线程的基本信息
- 直接查看
Thead
类的属性即可,主要关注的属性如下:
private volatile String name;// 线程名称,养成好的编程习惯,应该给线程命名利于调试
private int priority; // 线程优先级,越大优先级越高 1-10 默认为5
private boolean daemon = false;// 是否是守护线程
private long stackSize; // 线程堆栈大小 默认1MByte
private long tid; // 线程ID
private static long threadSeqNumber; // 静态的线程计数器,用于给新线程增加ID
public final static int MIN_PRIORITY = 1; //最小优先级
public final static int NORM_PRIORITY = 5;// 默认优先级
public final static int MAX_PRIORITY = 10;// 最大优先级
private volatile int threadStatus = 0; //线程的状态
//状态定义为Thead的一个内部枚举类:
public enum State{
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
}
- 线程分为
守护线程
和用户线程
除了守护线程就是用户线程,setDeamon(boolean on)
必须在start
调用前,否则会抛出异常,例如垃圾回收线程,程序运行完毕,JVM会等待用户线程退出而不会等待守护线程,jstack会包含'deamon'字样的为守护线程.
2.4 线程的状态
进入任意java线程的实现类均可查看到对应的状态,进入Thead.State
枚举类可查看各个状态和说明,各个状态如下:
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
-
NEW
新建后尚未启动的线程,即start
方法执行前的状态,需要注意的是对于执行start
方法后的线程状态是不确定的
,因为无法明确知道线程会进入哪种状态,测试过程如下:
// 封装成Runnable 供Thead创建线程
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread call = new Thread(futureTask, "futureTask");
int a = 1;
while (a == 1){
System.out.println(call.getState());// 中断打印出NEW
}
call.start();
-
TERMINATED
线程任务完成后,退出之后的状态。 -
RUNNABLE
中文翻译:RUNNABLE
状态表示线程正在java虚拟机中执行,但可能正在阻塞等待其他资源
,例如处理器。既然是等待资源,那么存在IO阻塞时线程状态也依然是RUNNABLE状态
,利用jsp查看java进程id,然后jstack id,即可查看该进程下的所有线程
.
"main" #1 prio=5 os_prio=0 tid=0x0303dc00 nid=0x3184 runnable [0x02e9f000] //线程信息概览
java.lang.Thread.State: RUNNABLE // 注意该处状态为 RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)// 以下为当前执行的方法栈
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
- locked <0x0a9d48d8> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x0a9b41e8> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
- locked <0x0a9b4188> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.newLine(PrintStream.java:546)
- locked <0x0a9b41e8> (a java.io.PrintStream)// 此处可以看出,print方法是线程安全的
at java.io.PrintStream.println(PrintStream.java:824)
- locked <0x0a9b41e8> (a java.io.PrintStream)// 此处可以看出,print方法是线程安全的
at com.simple.TicketDemo.main(TicketDemo.java:54)
-
BLOCKED
翻译线:线程状态被阻塞,等待监视器锁定。 *处于阻塞状态的线程正在等待监视器锁定输入同步块/方法或调用 {@link Object#wait()Object.wait}
后重新进入同步块/方法,阻塞状态不涉及进程外的阻塞(如IO阻塞),只描述JVM内部并发/主动休眠等原因导致的线程阻塞,3种细分:
(1)blocked
专指等待monitor
进入synchronized
块或方法的线程状态,利用上述jstack
的方法获取其状态如下:
java.lang.Thread.State: BLOCKED (on object monitor)
(2)waiting
,两个方法会导致线程进入该状态Unsafe.getUnsafe.park(boolean var1, long var2)
和Object.wait()
,前者用于阻塞某个线程,典型场景是使用了JUC包内提供的同步器或同步数据结构,它们的内部依赖LockSupport
类阻塞线程,该类进一步调用了Unsafe.park()
, 它的jstack
输出为:
java.lang.Thread.State: WAITING (parking)
后者jstack输出如下。Thread#join()
也是基于java自带的monitor/condition
机制实现的:
java.lang.Thread.State: WAITING (on object monitor)
(3)timed_waiting
,Unsafe.park()
和 Object#wait()
的超时版本或者Thead.sleep(long ms)
会让线程进入这个状态:
java.lang.Thread.State: TIMED_WAITING (sleeping)
2.5 线程常用的方法
-
Thead.currentThead()
获取当前代码段正在被哪个线程调用以及该线程的信息。 -
isAlive()
获取当前线程是否处于活动状态,活动状态指的是已经启动但未终止的状态,即线程状态转换图中的黄色、绿色以及红色状态。 -
sleep()
让当前线程处于休眠状态X毫秒(已经放弃持有锁,否则将占锁休眠)
,此处会放弃CPU资源,系统会再次调度线程CPU资源分配(不区分优先级)
。 - 静态方法
Thead.yield ()
让当前线程重新执行,即放弃CPU资源并再次获取,此时CPU资源只能被当前同优先级或者更高优先级的线程获得。 - 非静态的
join()
让一个线程 B “加入”到另外一个线程 A 的尾部。在线程 A 执行完毕之前,线程 B 不能工作,即在B中执行A.join()
,会使B线程阻塞至A线程执行完毕。
Thread t = new MyThread();
t.start();// 启动线程t
t.join();// 停止执行阻塞等待线程t执行完毕,若线程t没有存活则执行跳过。
-
sleep
和yield
方法的区别:
(1)sleep(long ms)
有参数,而yield()
无参数,二者都为Thead
类的静态方法。
(2)sleep
会放弃当前线程的CPU资源(无锁),其它所有线程均可争夺CPU资源,包括执行sleep(0)
,sleep(0)的可用于当前线程执行需要的资源还未完备时,暂时放弃执行,以提高CPU使用率
。
(3)yield
会放弃执行权,然后重新争夺CPU资源,此时只有与yield
同优先级或者高优先级的线程才可能拿到CPU资源。
(4)线程执行sleep
方法后转入阻塞(blocked
)状态,而执行yield
方法后转入就绪(ready
)状态.
2.5 停止线程
- 停止线程共有三种方法:
(1)使用stop()
暴力停止线程,立即停止一个正在运行的线程,该方法是不安全的,例如事务操作中强行停止一个线程可能造成意想不到的意外
,未及时释放锁资源将导致数据不一致
。
(2)使用标志,然后利用该标志判断是否需要return
。
(3)使用interrupt
和异常中断线程,该方法只提供一个标志位供线程使用,并不会直接停止线程。
class Ticket {
private int number = 3000000;
Lock lock = new ReentrantLock();
public void sale() {
//lock.lock();
//try {
while (number > 0) {
//Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "输出第 " + (number--) + " 张票");
}
//} catch (Exception e) {
// e.printStackTrace();
// } finally {
// lock.unlock();
//}
}
}
//Thread sale1 = new Thread(ticket::sale, "售票员1");
//sale1.start();
//sale1.interrupt(); 并不能停止目标线程,在线程中捕获interruptedException异常,可以安全的处理异常和退出线程
- 判断线程是否停止:
(1)Thread#interrupt()
方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态,执行后对应的线程会抛出interruptedExeception
,该异常可由线程捕获并做出对应的处理。
(2)Thread#interrupted()
静态方法,用于查询当前线程状态,并清除状态置为false
,若线程被中断,第一次调用返回true
,第二次调用就会返回false
.
// Thread.java
public static boolean interrupted() {
return currentThread().isInterrupted(true); // 清理
}
private native boolean isInterrupted(boolean ClearInterrupted);
(3)Thread#interrupted()
方法,查询中断状态,不会清除状态。
// Thread.java
public boolean isInterrupted() {
return isInterrupted(false); // 不清除
}
private native boolean isInterrupted(boolean ClearInterrupted);
2.2 线程的优先级与暂停线程
suspend()
与resume
可用于暂停与恢复线程执行,其具有独占与不同步的缺点,目前已经启用@Deprecated
线程的优先级会继承创建它的线程的优先级,CPU尽量将执行资源让给优先级高的线程,但不是全部具有一定规则性,说明线程优先级与代码块执行顺序无关,线程还具有随机性,即高优先级的线程不一定先执行完,总结为线程的优先级具有继承性、规则性、随机性三大特点。