1. 什么是线程?
线程: 在程序中负责执行具体任务的就是线程;线程是进程的基本执行单位,又叫做执行路径;
主线程: 负责执行一个程序的入口任务的线程就是这个程序的主线程;
2. 单线程: 如果一个程序从启动到结束,只有一个线程在运行,这个程序就是单线程的;
3. 多线程: 如果一个程序运行时要同时执行多个任务,就会创建多个线程,这个程序就是多线程的;
4. 线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
5. 并发和并行
并发:在一个时间段内同时发生;
并行:在一个时间点上同时发生;
6.同步和异步
同步: 程序执行时所有任务都顺序执行;如果一个功能中调用其它功能,在被调用功能结束前只能等待;
异步: 程序执行时任务不一定顺序执行;如果一个功能中调用其它功能,可以在被调用功能结束前就先返回;
7.Java实现多线程的两种方式
1)继承Thread类
实现步骤:
- 创建一个类,继承Thread类;
- 在这个类中重写run函数;
- 创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
/*
* 1、创建一个类,继承Thread类;
2、在这个类中重写run函数;
3、创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
*/
// 1、创建一个类,继承Thread类;
class MyThread extends Thread{
//2、在这个类中重写run函数;
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println("线程中的i=" + i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
//3、创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
MyThread mt = new MyThread();
mt.start();//启动线程
for (int i = 0; i < 5; i++) {
System.out.println("main中的i=" + i);
}
}
}
2)实现Runnable接口
实现步骤:
- 创建一个类实现Runnable接口;
- 在这个类中重写run函数;
- 创建这个类的对象;
- 创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
- 调用Thread类的start函数,启动线程;
/*
* 1、创建一个类实现Runnable接口;
2、在这个类中重写run函数;
3、创建这个类的对象;
4、创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
5、调用Thread类的start函数,启动线程;
*/
class MyTask implements Runnable {
// 2、在这个类中重写run函数;
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 5; i++) {
System.out.println(name + "---" + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 3、创建这个类的对象;
MyTask mt = new MyTask();
// 4、创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
Thread t = new Thread(mt);
// 5、调用Thread类的start函数,启动线程;
t.start();
//为了显示区别,在这个函数中也循环输出几个整数
for (int i = 0; i < 5; i++) {
System.out.println("主函数"+i);
}
}
}
两种实现方式中,实现Runnable接口方式的好处:
a.把线程任务和线程本身操作分离,避免了耦合。提高了代码的扩展性
b.避免了Java单继承的局限性(主要)
c.可以轻松的实现多线程间数据共享
8.实现多线程相关的一些问题
1)启动线程为什么要调用start而不是run?
因为run函数主要是定义这个线程需要执行的工作的,而不是创建线程,开辟栈空间的;
要创建线程,开辟栈内存空间等操作,都需要通过start函数来进行,而调用start函数启动一个线程之后,默认的就会去调用run函数
2)start方法和run方法的区别?
start用来启动线程,run用来书写线程工作逻辑代码的;线程启动后会自动调用run函数;
3)线程是否可以多次启动?
答案是会报java.lang.IllegalThreadStateException错误,即线程状态非法异常
参考:https://www.cnblogs.com/dennyzhangdd/p/7612194.html
4)run方法中出现异常怎么办?
因为Thread类中的run函数并没有声明编译异常,所以子类中重写run函数中如果有异常发生,只能捕获,不能声明出去;而且如果没有处理,这个线程就会结束,不会影响其它线程
5)sleep()和wait()方法的区别
四点:
A: wait可以不接收时间参数。sleep必须接收
B: wait需要别人唤醒才可以醒来。sleep可以等时间结束自动醒来
C: wait 需要跟同步技术结合使用,sleep不需要。
D: wait的线程释放锁,sleep不释放
联系:
都会进入阻塞状态,释放CPU的执行权和执行资格
6)为什么wait(),notify(),notifyAll()等方法都定义在Object类中
线程的等待和唤醒都必须有锁来控制
锁的对象是任意指定的。既然是任意对象都可以调用的方法,必须定义在Object中。
7)同步有几种方式,分别是什么?
4种:
方式1:同步代码块,锁是任意对象,必须唯一
方式2:同步方法,锁是this
方式3:静态同步方法,锁是当前类的字节码文件对象
方式4:Lock
lock和unlock
9.线程一个生命周期中的各种状态
java中一个线程在生命周期中经历的各种状态:
新建:通过new关键字创建线程对象,线程就进入新建状态;此时只是一个普通对象,没有分配栈内存空间;
就绪:通过线程对象调用start函数,就会正式启动这个线程,分配栈内存空间;可以调用run函数执行了;但是还没有获取到CPU的执行权;
执行:等到获取到CPU的执行权了,就开始正式执行;如果正在执行的线程失去了CPU的执行权,还会回到就绪状态;
阻塞:线程执行时,有可能遇到非常耗时的操作,或者其它线程调度,就会进入阻塞阶段;这个阶段的线程,没有CPU的执行权,也没有CPU的执行资格;当线程的阻塞条件失去,线程就会脱离阻塞阶段,进入就绪状态,等待获取CPU的执行权;
消亡:一个线程执行完所有任务,或者执行中出现异常,但是没有处理,这个线程就会今日消亡阶段;
10.线程调度
在计算机中,线程调度有两种方式,分别是分时调度和抢占式调度:
- 分时调度:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片;
- 抢占式调度:每个线程随机获取CPU使用权,然后执行时间也是不确定的;当一个线程失去CPU使用权后,再随机选择其它线程获取CPU使用权;
因为抢占式调度效率更高,所以一般多线程都使用这种方式实现线程的调度;
如果程序需要控制线程的执行过程,可以采用下面的方式来实现;
1)设置线程优先级
每一个线程都有一个优先级,高优先级线程的执行优于低优先级线程;
但是,高优先级的线程,只是获取CPU执行权的概率比其他线程高,不等于高优先级的线程一定先执行;
java中的线程优先级一供有十个,分别是1到10;数值越高,线程优先级越高;默认优先级是5;
//获取main线程的优先级
main.getPriority();
//设置main线程的优先级为10
main.setPriority(10);
2)线程让步
//一旦线程执行时调用这个函数,就会让出自己占用的CPU执行权,就进入就绪状态
yield()
3)线程休眠
//如果线程执行时调用这个函数,会进入阻塞状态,线程就失去执行权和执行资格;
//休眠函数会接收一个时间参数;时间到了,会自动苏醒,就进入就绪状态;
Thread.sleep(5000);//单位是毫秒
4)线程插队
//等待线程t2执行结束后再执行后续线程
t2.join();
5)设置线程为守护线程
守护线程和用户线程的概念:
有的程序在执行的时候,需要执行某个特殊任务,这个任务不管执行完毕没有,只要程序中的其他线程都结束了,这个任务也一定会结束;
负责执行这个任务的线程,就是守护线程;
相对应的,非守护线程就是用户线程;
//设置线程t为守护线程
//因为t线程被设置为守护线程,所以不管这个线程中的任务有没有执行完毕,只要所有非守护线程结束,守护线程也会结束;
t.setDaemon(true);
//测试该线程是否为守护线程
t.isDaemon();
11.线程安全
线程安全问题出现的基本前提:
- 存在多线程;
- 多个线程都要操作同一个数据,而且有修改操作;
- 多个线程对共同数据的修改操作不是同一行代码,CPU在这几行代码之间存在来回切换;
解决方法:使用同步锁
class SellTicketTask2 implements Runnable{
private int num = 100;//被操作的数据
private Object obj = new Object();
public void run() {
//获取线程名字
String name = Thread.currentThread().getName();
while(num > 0){
synchronized(obj/*这个对象就叫做同步锁,一个同步锁同时只能被一个线程获取*/){
if(num > 0){
//输出一句话,表示卖出票
System.out.println(name + "售出第"+num+"张票");
num--;
}
}
}
}
}
12.懒汉式实现单例的线程安全问题
单例模式回顾:
1)饿汉式:
class Single{
private Single(){}
private static Single ss = new Single();
public static Single getInstance(){
return ss;
}
}
好处:类加载时创建对象,可以保证对象的唯一性;
弊端:类加载时创建对象,容易造成内存浪费;
2)懒汉式:
class Single{
private Single(){}
private static Single ss = null;
public static Single getInstance(){
//多线程中会出现的问题
if(ss == null){
//当线程1执行到这一步时,CPU执行权被抢走,线程2执行上面代码发现ss==null为true,继续执行创建一个对象,此时线程1重新夺回CPU执行权,继续执行下面代码,又创建新的对象,不能保证对象的唯一性
ss = new Single();
}
return ss;
}
}
好处: 调用获取函数时才创建对象,可以避免内存浪费
弊端: 多线程环境下不能保证对象的唯一性;
懒汉式多线程中存在的问题解决:
class Single{
private Single(){}
private static Single ss = null;
private static Object lock = new Object();
public static Single getInstance(){
if(ss == null){
//加同步锁
synchronized(lock){
if(ss == null){
ss = new Single();
}
}
}
return ss;
}
}
13.JDK5关于锁和等待唤醒的升级
Condition接口
14.线程池
什么是线程池技术呢?
就是一种容器,可以自己管理线程的创建和销毁的工作;
里面的线程创建后,可以执行我们提交给线程池的任务;
当一个线程执行完一个任务后,不会立即死亡,而是进入空闲期,继续生存一段时间;
这个时候如果我们又提交的新的任务,就不需要创建新的线程,直接拿空闲的线程来执行这个任务;
1)和线程池有关的几个概念:
最大容量:一个线程池中最多可以保存的线程数量;
最小容量:一个线程池中,最少保存的线程数量;
最大存活时间:一个线程最多可以保持空闲的时间;如果超出这个时间,一般这个线程就会被销毁;
2)线程池的创建
这个函数创建的线程池,一开始默认是没有线程;但是当需要使用线程时,线程池就会创建一个新的线程返回,当这个线程被使用完毕后,不会消亡,而是回到这个线程池中,在一分钟之内,如果有新的任务,就可以拿这个线程去执行任务;如果超过1分钟没有使用,这个线程就会挂掉;
这个方法可以根据参数创建一个指定大小的线程池;
如果需要处理的任务超过这个数量,也就是所有线程都在工作,那么新的任务会在一个队列中等待;
如果所有线程都执行完毕,都不会消亡,一致保存,知道线程池被关闭;
如果执行任务中出现异常,造成线程消亡,那么线程池会创建一个新的线程补上;
这个函数创建的线程池,只会在里面保存一个线程;
注意:线程池启动后执行完任务,不会直接结束程序,要想结束,需要手动关闭线程池;
3)带返回值的线程任务——Callable接口
这个接口就是定义要逛街线程任务的,只是这个线程任务可以返回一个结果;还可以声明编译期异常;
提交任务对象的方法:
Future接口
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建一个线程池对象,用来处理Callable接口定义的线程任务
ExecutorService es = Executors.newCachedThreadPool();
// 创建具体的线程任务
Callable<Integer> c1 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
Thread.sleep(100);
sum += i;
}
return sum;
}
};
Callable<Integer> c2 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 11; i <= 20; i++) {
Thread.sleep(100);
sum += i;
}
return sum;
}
};
//将线程任务提交到线程池;同时返回一个封装了任务结果的Future对象
long l1 = System.currentTimeMillis();
Future<Integer> f1 = es.submit(c1);
Future<Integer> f2 = es.submit(c2);
//通过Future对象获取返回的结果
Integer i1 = f1.get();
Integer i2 = f2.get();
System.out.println(i1 + i2);
long l2 = System.currentTimeMillis();
System.out.println(l2 - l1);
//关闭线程池
es.shutdown();
}
}