【总结】并发编程(一:基础)
1.线程状态
笼统的可以分为 新建 就绪 运行 阻塞 消亡五个状态
(1)新建:当需要新起一个线程来执行某个子任务时,就创建了一个线程
(2)就绪:线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态(java中调用start()方法进入就绪状态)
(3)运行:当得到CPU执行时间之后,线程便真正进入运行状态
(4)阻塞:运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)
(5)消亡:突然中断或者子任务执行完毕,线程就会被消亡

2.线程切换
对于单核CPU来说,CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。因此需要记录线程A的运行状态,因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素
3.Thread类中的方法
1.start():start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源
2.run():run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务
3.sleep():sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:
如果调用Thread.currentThread().sleep(10000); t1阻塞10秒后,执行完run方法,t2才会进入同步块执行
如果调用object.wait();当前线程回释放这个对象锁,所以会连续打印两句:t1获取对象锁xx t2获取所对象xx
public class Test implements Runnable{
Object object = new Object();
public static void main(String[] args) {
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
}
@Override
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"获取对象锁,接下来调用sleep阻塞");
try {
// Thread.currentThread().sleep(10000);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}
}
4.sleep()和wait()的区别?
(1)sleep()是Thread的方法,wait()是Object的方法
(2)sleep()可以用在任意位置,调用wait()方法之前,线程必须要获得该对象的对象级锁;换句话说就是该方法只能在同步方法或者同步块中调用
(3)sleep()阻塞线程的同时不释放锁对象,wait()阻塞线程,释放锁对象
5.yield:让当前运行线程回到就绪状态(释放cpu)
6.join:t1调用t2的join方法,会先执行t2,然后执行t1.有三个重载版本。
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间
7.setDaemon():设置是否成为守护线程
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
4.创建线程
1.方式一:集成Thread类
package com.thread;
//通过继承Thread类实现自定义线程类
public class MyThread extends Thread {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by extends Thread");
}
public static void main(String[] args){
//实例化自定义线程类实例
Thread thread = new MyThread();
//调用start()实例方法启动线程
thread.start();
}
}
优点:实现简单,只需实例化继承类的实例,即可使用线程
缺点:扩展性不足,Java是单继承的语言,如果一个类已经继承了其他类,就无法通过这种方式实现自定义线程
2.方式二:实现runnable接口
package com.thread;
public class MyRunnable implements Runnable {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by implements Runnable");
}
public static void main(String[] args){
//线程的执行目标对象
MyRunnable myRunnable = new MyRunnable();
//实际的线程对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
}
}
优点:扩展性好,可以在此基础上继承其他类,实现其他必需的功能
缺点:构造线程实例的过程相对繁琐一点
3.方式三:实现callable接口
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello, I am the defined thread created by implements Callable";
}
public static void main(String[] args){
//线程执行目标
MyCallable myCallable = new MyCallable();
//包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//传入线程执行目标,实例化线程对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
String result = null;
try {
//获取线程执行结果
result = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
}
}
优点:具备返回值以及可以抛出受检查异常
缺点:相较于实现Runnable接口的方式,较为繁琐

浙公网安备 33010602011771号