一.进程和线程
进程(process)是操作系统的任务单元,每个程序启动后,系统都会为其分配一个进程编号PID。
线程(Thread)是进程中的任务单元,当程序启动以后,首先会创造主线程,可以在主线程中开辟子线程,每个线程都对应一个虚拟机栈,栈是线程私有的,堆和方法区是线程共享的。
二.串行和并行
串行:在一台机器上单线程执行
并行:
并发:在同一台机器上多线程并行执行(存在资源竞争关系)
并行:在多台机器上并行执行(不存在资源竞争关系)
二.Java中实现多线程的方式有4种
1.继承Thread类,重写run方法
public static void main(String[] args) {
System.out.println("主线程开始");
MyThread mt = new MyThread();//开辟子线程
//mt.start();//start是子类从父类中继承的方法,start在子线程中调用run
mt.run();//在当前主线程中调用run
System.out.println("主线程结束");
}
//静态内部类
//实现线程的方式1:继承Thread类,重写run方法
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("子线程开始");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程结束");
}
}
2.实现Runnable接口,重写run方法
public static void main(String[] args) {
System.out.println("主线程开始");
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();//调用start
//t.run();
System.out.println("主线程结束");
}
//静态内部类
//实现线程的方式2:实习Runnable接口,重写run方法
//Dog的父类是Animal,这时Dog还能继承Thread吗?
//官方给出的解释,当这个类继承了父类,此时不方便继承Thread,可以采用实现Runnable接口的方式
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("子线程开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程结束");
}
}
3.实现Callable接口,重写call方法
public static void main(String[] args) {
MyCallable mc = new MyCallable();//创建一个Callable对象
FutureTask<Integer> ft = new FutureTask<>(mc);//创造一个FutureTask对象,用Callable
//对象作为构造方法
Thread t = new Thread(ft);//构造一个Thread对象,用FutureTask对象作为构造方法的参数
t.start();//用FutureTask对象的get()方法取出子线程call方法的返回值
//Integer sum = ft.get(),会阻塞主线程
while (true) {
try {
if (ft.isDone()) {//isDone()方法可以判定子线程有没有结束
Integer sum = ft.get();//在取子线程返回值的时候,子线程可以正常返回,也可
//以异常返回
System.out.println("子线程返回值:" + sum);
break;
} else {
System.out.println("子线程还没有结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//静态内部类
//实现线程的方式2:实习Callable接口,重写call方法
//需要指定泛型,这个泛型是作为子线程返回值的数据类型
//实现线程的方式1和方式2都无法实现子线程运行结束后返回一个结果,因为run方法的返回值类型是void
static class MyCallable implements Callable<Integer> {
public Integer call() throws Exception{
Integer sum =0;
for (int i = 0; i < 100; i++) {
sum += i;
Thread.sleep(100);
}
return sum;
}
}
4.使用线程池类
三.实现多线程的方式的区别:
在当我们要对一个类执行多线程操作时,我们可以让它继承Thread,通过重写run方法创建一个子线程实现,但如果当这个类已经有父类对象时,便不方便继承Thread,这时我们采用实现Runnable接口的方式,重写run方法。
当我们需要通过子线程返回一个返回值时,因为以上方式为void类型没有返回值,所以我们采用实现callable接口的方式,重写call方法
四.线程的五种生命周期状态图
五、线程安全
我们把数据类型分为线程安全类型和线程不安全类型
如果一个数据类型需要我们手动加锁来保证其操作的原子性,那么它就是线程不安全的数据类型,反之,它就是线程安全的。
单线程,多线程不共享是不需要考虑线程安全问题,只有在多线程共享数据的前提下,才需要考虑加锁的问题
线程不安全类型 | 线程安全类型 |
ArrayList | 1.Vector (synchronized) 2.CopyOnWriteArrayLIst (Lock) |
HashMap | Hashtable (synchronized) 2.ConcurrentHashMap (Lock) |
String,StringBuilder | StrinfBuffer |
Int,Integer | Atomiclnteger |
其它................... |
六.锁
锁是Java中用来保证线程操作原子性的一种机制,包含同步锁synchronized和Lock锁
原子性:指的是整个程序中的所有操作,要么完全执行,要么完全不执行,不可能停滞在中间某个环节的特性。
synchronized是关键字,可以锁住方法或代码块用法:
public class SuoDemo01 {
public static void main(String[] args){
CountRunnable runnable =new CountRunnable();
for (int i = 0; i < 10000; i++) {
//开辟了10000个线程,共享同一个runnable对象
Thread t =new Thread(runnable,"子线程+1");
t.start();
}
}
static class CountRunnable implements Runnable{
static int i =0;//静态变量i在方法区,被各个线程共享
// int i =0;//成员变量在对象中,对象在堆中
@Override
public synchronized void run() {//同步锁,锁方法
// int i =0;//局部变量i,在方法入栈后,各个线程创建私有的i进行操作
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
Lock是类(官方推荐),只能锁代码块
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SuoDemo01 {
public static void main(String[] args){
CountRunnable runnable =new CountRunnable();
for (int i = 0; i < 10000; i++) {
//开辟了10000个线程,共享同一个runnable对象
Thread t =new Thread(runnable,"子线程+1");
t.start();
}
}
static class CountRunnable implements Runnable{
Lock lock =new ReentrantLock();//声明一个锁对象,让它成为成员变量让各个子线程共享
static int i =0;//静态变量i在方法区,被各个线程共享
// int i =0;//成员变量在对象中,对象在堆中
@Override
public void run() {
// int i =0;//局部变量i,在方法入栈后,各个线程创建私有的i进行操作
lock.lock();//上锁
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
lock.unlock();//开锁
}
}
}
七、死锁
产生死锁的原因:
简单来说,就是两个线程各自持有一个对象的锁,且双方都还未释放的情况下又去请求对方的锁,从而造成双方循环等待的局面。
1.死锁产生的条件:
(1)互斥条件:锁要具有排它性,即一个资源只能被一个线程占用,直到被该线程释放
(2)请求和保持条件:一个线程因为请求占用资源而发生阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:线程在已获得资源但未使用完的前提下不能被其他线程强行剥夺,只有在使用 完毕后才会释放资源
(4)循环等待条件:当某个线程和另一个线程互相都不满足释放条件的情况下,互相去请求对方 持有的锁时,就会进入一个环路,双方循环等待,造成永久阻塞
如何解决死锁:(破坏一个条件即可)
(1)破坏互斥条件:使用共享锁,在同一时刻,锁可以被多个线程持有
(2)破坏请求与保持条件:一次性申请所有资源
(3)破坏不剥夺条件:一个线程在请求其它资源的时候被阻塞,那么会主动释放它占有的资源
(4)破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件
小练习:
实现多线程的4种方式:1._________;2.__________3.__________4.___________
线程的5种生命周期:1._______2._______3._______4.________5._________
一般情况下,__________变量储存在方法区中,当前类中可以被各线程共享;若创建一个对象,该对象的_________变量储存在堆中,可以被各线程共享;__________变量可以在方法入栈后,被各个线程进行________操作
有关线程安全判断题:
单线程以及多线程不共享都是需要考虑线程安全问题。 ( )
一个数据类型如果需要我们手动加锁来保证其操作的原子性,那么它就是线程不安全的数据类型
( )
实现多线程的几种方式的区别(不考虑线程池方法):