并发编程
多进程和多线程是实现并发编程的两种技术手段。例如,每一个客户端请求发送到服务器上,服务
器提供一个进程,给这个客户端进行服务
线程和进程
通过对操作系统的初步认识,我们可以知道操作系统是多任务操作系统,同时CPU是操作核心,因
此通过多进程编程方式,可以实现"并发编程"的效果。
进程整体是一个比较"重"的概念,创建进程/销毁进程开销比较大,尤其是对于频繁的创建销毁进程
为了解决上述问题,引入线程(Thread),轻量级进程,这种创建销毁的开销更小
每个进程,都相当于是要执行的任务;每个线程,也是一个要执行的任务(运行一段代码指令)
进程和线程的区别
- 进程包含线程,进程和进程之间所涉及到的资源则是各自独立的,确保了进程的稳定性,相互之间是不干扰的
- 进程是操作系统分配资源的基本单位,线程是CPU上调度执行的基本单位,资源分配包括:CPU,内存,硬盘资源(文件描述符)...,每个线程上都会有状态,优先级,记账信息,上下文这样调度相关的数据。但这些数据都会共用同一个文件描述符和内存指针
- 进程之间管辖的多个线程之间,会共享上述的内存资源和硬盘资源,网络宽带
- 进程创建,需要申请资源;进程销毁,需要释放资源
- 对于线程来说,只是第一个线程创建的时候(和进程一起创建的时候)申请资源,后续再创建线程,不涉及到资源申请操作(干的是少,快)
- 只能所有的线程都销毁(进程销毁)才真正释放资源,运行过程中销毁某个线程,也不会释放资源
- 一个进程挂了一般不会影响到其他进程, 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃)
多线程的不足
- 虽然提高线程的数目,能提升效率,也不是"线性增长",线程数目达到一定程度之后,就算线程再多,也没法起到效果
- 线程数目如果太多,线程的调度开销也会非常明显。因为调度开销拖慢程序的性能。
- 多个线程处理任务时,可能会出现线程安全问题/线程不安全,这样的冲突可能会导致代码出现bug
- 一个线程抛出异常·,可能会带走整个进程所有的线程都无法继续工作,但是如果及时捕获,处理掉,也不一定会导致进程终止
线程的实现
线程的实现
Thread类
线程是操作系统提供的概念,操作系统提供一些原生线程api,java对这些api进行了进一步的抽象
和封装,形成了Thread类
javaclass Mythread extends Thread{ public void run(){ System.out.println("hello thread"); } } public class demo1 { public static void main(String[] args) { Thread t=new Mythread(); t.start(); System.out.println("hello main"); } }

Thread父类中,本身有一个run方法,程序员编写自己的逻辑,替代自身的run
start创建了一个新线程,多了一个执行流,能够干活(这个代码可以"一
心两用"同时做两件事)
第一个线程是main,第二个线程就是我们自己创建的MyThread


sleep()
sleep是Thread的静态方法,意思是让程序休眠,让当前的线程暂时放弃CPU,休息一会,时间过
了之后再执行
java
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
但要注意的是,调用sleep方法是会抛出异常,要进行异常捕获

对于main方法,我们可以用throws或try catch;但是在MyThread类中我们只能try catch,无法进
行throws,因为父类Thread run中,没有throws


有时候是main在前,有时候是thread在前。对于这个"抢占式执行"现象,是因为多个线程调度顺序
是随机的,这两线程谁先执行谁后执行都有可能,无法进行准确的预测
这个调度顺序是操作系统内核的调度器控制的,咱们无法在应用程序中编写代码控制,唯一能做的
是给线程设置优先级,但操作系统也不一定会严格执行
线程的运行


这个操作并没有创建线程,只是调用刚才重写的run,此时整个进程中只有一个main线程
main方法对应的线程就是一个进程至少要包含的那个线程,即主线程
使用jconsole方法观察线程

main和Thread-0线程是我们创建的线程,其他线程都是JVM内置的线程,启动任何一个java进程,都会自动带这些线程

线程的调用栈,获取线程状态的时刻,线程里的代码执行到哪了


通过调试,我们可以关注我们想要关注的线程
创建线程的写法
1.继承线程的写法,重写run
java
class Mythread extends Thread{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Mythread();
t.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
真正在系统中创建线程,JVM调用操作系统的API完成线程创建的操作

线程的入口方法,新的线程启动,就要执行这里的代码,run相当于"回调函数"
2.实现Runnable,重写run
java
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class demo2 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}

是一个要执行的任务,就是一段要执行的逻辑,最终还是要通过Thread来创建真正的线程,线程要干啥,通过Runnable来表示(而不是通过直接重写Thread run来表示)
线程要执行的任务的定义放在Thread里面是继承,放到外面是Runnable
这种方式能更好的解耦合,代码修改会更加简单
3.使用匿名内部类
本质上就是方法一,使用匿名内部类

在{}里面就可以编写子类的定义代码,子类里有哪些属性,要有哪些方法,重写父类的哪些方法
创建了这个匿名内部类的实例,并且把实例的利用赋值给t
java
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
这种代码具有一次性的特点,只能在匿名内部类使用一次
4.使用Runnable,匿名内部类
java
public class demo4 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable=new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t=new Thread(runnable);
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
使用Runnable,任务和线程的概念是分离的
5.针对三和四进一步改进,引入lambda表达式
本质上就是一个"匿名函数",最主要的用途就是作为"回调函数"
()->{}叫做"函数式接口",创建一个匿名的函数式接口的子类,并且创建出对应的实例,并且重写了里面的方法
java
public class demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}