1、 进程、线程、协程的概念
1.1、什么是进程?
简单的来说,我们在电脑上安装了一个软件,如:QQ,这是一个程序,程序是一个静态的概念,你不去操作他,他就是一个简单的二进制文件,但是当你去双击运行QQ的时候,他就被加载到内存中,这个时候他就是一个进程,相对程序来说他是一个动态的概念,他是需要占用系统资源的。
1.2、什么是线程?
在早期的操作系统中,CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。但是计算机技术的发展,人们再也不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务,这就很影响效率。这个时候线程的概念就被提出来了,让一个线程去执行一个子任务,一个进程包含多个线程,多个线程共享进程的内存空间。当我们想播放视频的时候,就放播放视频这个线程去执行,如果同时想下载视频,那么就可以暂停播放视频的线程,让下载视频的线程去执行,执行一会再切换回来,继续播放视频,循环反复,由于CPU采用时间片调度算法,切换非常快!,我们几乎是感觉不到变化的,所以给我们的感觉就是在播放视频的同时在下载视频。
1.3、协程
协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 (后面补充)
2、Java创建线程的方式
public class CreateThreadDemo {
private static class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
private static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello MyRunnable!");
}
}
private static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("Hello MyCallable!");
return 666;
}
}
public static void main(String[] args) {
//1、第一种方式,继承Thread,重写run方法
new MyThread().start();//output:
//2、第二种:实现Runnable,重写run方法,创建Thread对象,传入Runnable实现类对象
new Thread(new MyRunnable()).start();
//3、第三种:实现Callable,重写call方法,
new Thread(new FutureTask<>(new MyCallable())).start();
//4、第四种:利用线程池,后面单独介绍(不建议在项目中直接使用Executors.newCachedThreadPoll这种方式)
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(()->{
System.out.println("Hello ThreadPool");
});
executorService.shutdown();//关闭池类资源
}
}
3、Thread类的常用方法
这里介绍一下Thread 的几个关键方法。
- start():开始执行线程的方法,java虚拟机会调用线程内的run()方法,然后操作系统会创建相应的线程;
- yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方 -法,程序在调度的时候,也还有可能继续运行这个线程的;
- sleep():静态方法,使当前线程睡眠一段时间;
- join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
public class ThreadUsualMethodDemo {
public static void main(String[] args) {
//testSleep();
//testYield();
testJoin();
}
/*Sleep,意思就是睡眠,当前线程暂停一段时间让给别的线程去运行。Sleep是怎么复活的?由你的睡眠时间而定,等睡眠到规定的时间自动复活*/
static void testSleep() {
new Thread(()->{
for(int i=0; i<100; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
//TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
/*Yield,就是当前线程正在执行的时候停止下来进入等待队列,回到等待队列里在系统的调度算法里头呢还是依然有可能把你刚回去的这个线程拿回来继续执行,当然,更大的可能性是把原来等待的那些拿出一个来执行,所以yield的意思是我让出一下CPU,后面你们能不能抢到那我不管*/
static void testYield() {
new Thread(()->{
for(int i=0; i<100; i++) {
System.out.println("A" + i);
if(i%10 == 0) Thread.yield();
}
}).start();
new Thread(()->{
for(int i=0; i<100; i++) {
System.out.println("------------B" + i);
if(i%10 == 0) Thread.yield();
}
}).start();
}
/*join, 意思就是在自己当前线程加入你调用Join的线程(),本线程等待。等调用的线程运行完了,自己再去执行。t1和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2去运行,t1等待t2运行完毕继续t1运行(自己join自己没有意义) */ (引狼入室^-^)
static void testJoin() {
Thread t1 = new Thread(()->{
for(int i=0; i<100; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
//TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0; i<100; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
//TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
4、线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZOyKPDS-1604423408436)(https://oscimg.oschina.net/oscnet/up-ab5d5f26206f81e661a93d9dc71763b3700.png “线程生命周期图”)]
5、线程同步
5.1、多线程存在的问题
由于多线程是共享内存空间的,很多公共资源就不会为每个线程单独创建,这个时候多个线程去操作同一个共享资源,如果没有一个合理的规则,就可能会存在问题。
比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1啊,如果两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候而第二个线程读到了它还是0,加1在写回去,本来加了两次,但还是1,那么我们在对这个数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它计算,我必须加完1收释放锁,其他线程才能对它继续加。
5.2、Synchronized关键字
为了解决多线程情况下操作共享资源的安全性,Java提供了一些方法可以保证多线程操作共享资源的安全性,如Synchronized关键字。
案例(使用synchronized保证共享资源count最终的值为0,且每个线程打印的都是最新的值):
5.2.1、Synchronized用在普通方法上
public class SynchronizedDemo {
private int count = 100;
public synchronized void printCount() {
System.out.println(Thread.currentThread().getName()+",count = " + count--);
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
for (int i = 0; i < 100; i++) {
new Thread(()->{
demo.printCount();
}).start();
}
}
}
5.2.2、Synchronized用在静态方法上
public class SynchronizedDemo {
private static int count = 100;
public static synchronized void printCount() {
System.out.println(Thread.currentThread().getName()+",count = " + count--);
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
SynchronizedDemo.printCount();
}).start();
}
}
}
5.2.3、Synchronized用在代码块上
public class SynchronizedDemo {
private int count = 100;
private static final byte[] lock=new byte[0];
public void printCount() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ",count = " + count--);
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
demo.printCount();
}).start();
}
}
}
Synchronized底层原理解析
Jdk1.5之前,Synchronized的底层是直接向操作系统申请所资源来完成的,使用起来比较耗费资源,JDK1.5之后,官方对Synchronized进行了一系列的优化,主要是新增了一个锁升级的过程。
这个锁升级的过程,将状态存储在在对象头上的markword中。
升级过程如下:
- 无锁:当一个共享资源没有被访问时,锁状态无变化,无锁态
- 偏向锁:当有线程多次对共享资源进行访问时,则升级为偏向锁,markword存储线程的id和epoch。
- 轻量级锁: 当偏向锁的时候,如果有其他线程来竞争,则升级为自选锁,用户态,耗费CPU资源,不会阻塞,默认自选超过10次则升级为重量级锁,但是次数不是固定的,如JDK1.7之后推出了适应性自选,可以智能的采用合适的次数作为阈值。
- 重量级锁:当竞争的线程变得很多,升级为重量级锁时,所有需要获取资源的线程都加入到操作系统的队列中,不耗费CPU资源,由操作系统进行资源分配。