多线程的学习
1、要学习多线程,就要知道什么是线程
一、 知道线程是什么,就要先了解进程和线程
1、进程:进程就是一个实例,例如idea就是进程,进程可以开多个或者一个,idea就可以开多个,任务管理器就只能开一个
2、线程:线程是进程内真正要去执行的操作,比如,要执行一段for循环,真正是由线程需执行的,线程可以有多个,多个线程可以并行或并发的处理方法,以下我们解释什么叫并行和并发
二、了解并行和并发
1、并发:单核处理器下,线程轮流使用cpu的情况叫做并发, 比如家庭主妇,同一段时间需要洗衣服,洗碗,这就叫并发
2、并行:多核处理器下、一个线程使用一个cpu的情况叫做并行,比如家庭主妇请了个保姆,可以帮助洗碗,这时,保姆可以洗碗,家庭主妇可以洗衣服,同时做两件事,叫并行
比如在单核的处理器下,开启了多个线程,执行速度不会比开一个线程块
三:多线程优缺点
1、优点:可以同时访问多个方法,不需要再去等待一个方法执行完,再去执行另外一个方法
2、缺点:多线程是不安全的,原因是java处理数据的机制导致的,以下举例
例如在一个线程处理 a = 1的时候java是如何处理的:
第一步:将内存中a放在寄存器中
第二步:将寄存器中的a + 1,此时也就得到了a = 2
第三步:将寄存器中的a返回给内存中 此时比如开启了第二个线程也要将a+1 ,第二个线程也去拿a,例如此时可能线程1正进行到第二步,还没有把a = 2放回到内存中,进程2拿到的值还是1,就会出现数据错误的情况
下方代码测试,用两个线程操作将1加到100 测试结果:不用线程正常需要50次,结果是五十多次,以此证明是不安全的:
public static void main(String[] args) {
//查看线程执行顺序 结果:执行没有顺序 两个线程操作将1加到100 正常需要50次,结果是五十多次,以此证明是不安全的
int count = 0;
AtomicInteger a = new AtomicInteger(1);
while (true){
count ++ ;
new Thread(() -> {
a.getAndIncrement();
System.out.println(Thread.currentThread().getName() + " run,加到了:" + a);
}, "thread2").start();
new Thread(() -> {
a.getAndIncrement();
System.out.println(Thread.currentThread().getName() + " run,加到了:" + a);
}, "thread3").start();
if(a.intValue() >= 100){
break;
}
}
System.out.println("增加100个 操作了 :" + count + "次");
}
四:创建线程的方式
1、匿名内部类方式
public static void main(String[] args) {
Thread thread1 = new Thread("thread1") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run");
}
};
thread1.start();
}
2、Runnable与thread配合的方式创建线程,更灵活,推荐使用
//第一种 runnable的匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "run");
}
};
Thread thread2 = new Thread(runnable, "thread2");
thread2.start();
//第二种 lambda方式 更简洁
Thread thread3 = new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "run");
}, "thread3");
thread3.start();
五:线程的常用方法
1、start():线程启动需要用到的方法
2、run():项目启动后会调用的方法,如果将start改成run,则线程不会启动,执行后还会是主线程在打印
3、join():等待线程结束,等待该线程后才能启用其他线程,以下例子为 thread1睡眠了一秒钟,正常是thread2先执行完,加上join,就会是线程2等待线程1
4、join(Long n):等待线程运行结束,最多等待几秒
5、sleep(int n):睡眠一段时间,此时时间片会被让出,不会释放锁,睡眠结束后不一定会立即执行,会等待cpu获得到cpu的使用权,也可使用 TimeUnit.SECONDS.sleep(1);方法来睡眠,SECONDS可以定义单位为秒,分等等,更方便使用
6、interrupt():增加打断线程标记,如果判断线程正在sleep,wait,join,会导致被打断的线程抛异常,并且清除打断标记。如果打断正在进行中的线程,则会设置打断标记。(关闭时使用)
7、isInterrupted():判断当前线程是否被打断,不会清除打断标记(关闭时使用)
8、interrupted():判断当前线程是否被打断,会清除打断标记
9、setDaemon();设置为守护线程,默认为非守护线程,守护线程会跟着主线程的终止而终止
10、yield();让出当前正在执行的线程,并执行其他线程
11、getStatus():获取线程中的状态
1. 初始(NEW):刚new出来的线程
2. 运行(RUNNABLE): 获得了cpu的时间片,正在进行中的线程。注意,没有抢到cpu时间片的线程也是运行状态
3. 阻塞(BLOCKED):如果第一个线程获得锁,第二个线程再去获取这个锁的时候就会进去阻塞状态。
4. 等待(WAITING):例如join方法、sync锁都会导致线程进入等待状态
5. 有时限等待(TIMED_WAITING):例如调用Thread.sleep(1)方法 会导致线程进入超时等待状态
6. 终止(TERMINATED):表示该线程已经执行完毕。
六:优雅的关闭线程(两阶段终止)
错误:使用stop停止线程,会直接将线程杀死,没有机会再去处理线程死亡后的事情
正确:使用interrupt方法设置打断标记,isInterrupted判断如果为以打断,则跳出循环,跳出前可以对线程作出处理,此时线程被自然关闭。注意,需要在catch块中也设置打断标记,也就保证了即使异常了也会正常退出线程了
public static void main(String[] args) {
new Thread(() ->{
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "开启线程");
//循环查看这个线程是否被关闭了
while (true){
try {
//判断这个线程是否被打断
if(thread.isInterrupted()){
System.out.println(thread.getName() + " -- 我被打断了");
break;
}
//处理自己的逻辑代码开始
Thread.sleep(2000);
int a = 1/0; //测试处理业务时出现了异常是否会正常结束线程
System.out.println(thread.getName() + "执行业务代码");
//处理自己的逻辑代码结束
thread.interrupt();
System.out.println(thread.getName() + "打断线程");
}catch (Exception e){
e.printStackTrace();
//如果出现了异常 调用interrupt方法再次标记为打断状态
thread.interrupt();
}
}
},"线程1").start();
}
七:线程的查看以及测试方法
1、查看线程信息
1、 linux操作系统下使用 top -H -p 进程号(PID) ,进程号获取方式:ps -ef | grep 端口
2、 java给我们提供了查看线程的工具,在window 下 window + R 输入jconsole 可以查看所有线程状态,提示:需要先打开线程 在输入jconsole
2、测试线程
1、在开启线程的debug小红点上,右键切换为thread的debug模式
2、之后开启debug运行,在最下方Debug控制台中,找到Debugger下的Frames,可以切换每个线程进行执行