线程
线程的六种状态
说明:一般教科书和很多网上都说是五种,但查阅Java底层代码显示是六种,总体含义差不多,就采用六种方式
新建状态(NEW)
可运行状态(Runnable)
阻塞状态(Blocked)
无限期等待(Waiting)
限期等待(Timed Waiting)
死亡状态(Terminated)
线程状态图转换
线程的运行示意图
使用线程的三种常用方法
在了解如何使用线程之前,先理解下线程和非线程的区别
非线程
public class A {
public static void main(String[] args) {
for(int i=0;i<5;i++){//循环5次
System.out.println("A: "+i);
}
for(int j=0;j<5;j++){//循环5次
System.out.println("B: "+j);
}
}
}
输出结果:
我们可以发现,两个for循环语句都是在主线程中执行的,他们的执行顺序是串行的,所以不会发生交替现象。
伪多线程
public class B extends Thread{//继承Thread类
@Override
public void run(){//重写run方法
for(int i=0;i<5;i++){//for循环5次
System.out.println("A: "+i);//打印线程结果
}
}
public static void main(String[] args) {
B b=new B();
b.run();
b.run();
}
}
输出结果:
这里虽然使用Thread,但并没有实现多线程,只是顺序执行两次而已
继承Thread类
public class A extends Thread{//继承Thread类
@Override
public void run(){//重写run方法
for(int i=0;i<100;i++){//for循环100次
System.out.println("A: "+i);//打印线程结果
}
}
public static void main(String[] args) {
A a1=new A();//实例化线程a1
a1.start();//执行线程方法
A a2=new A();//实例化线程a2
a2.start();//执行线程方法
}
}
输出结果:
我们可以看到,两次执行线程出现交叉现象,说明是多线程程序
实现Runnable接口
public class A implements Runnable{//继承Thread类
@Override
public void run(){//重写run方法
for(int i=0;i<100;i++){//for循环100次
System.out.println(Thread.currentThread().getName()+"线程: "+i);//打印线程结果
}
}
public static void main(String[] args) {
A a1=new A();//实例化线程a1
Thread t1=new Thread(a1);//实例化Thread类,将线程a1作为参数传入
//通过调用Thread下的方法进行实现
t1.setName("A");//设置线程名称
t1.start();//执行线程方法
A a2=new A();//实例化线程a2
Thread t2=new Thread(a2);//实例化Thread类,将线程a2作为参数传入
t2.setName("B");//设置线程名称
t2.start();//执行线程方法
}
}
输出结果:
实现Callable接口
public class C {
public static void main(String[] args) {
//实例化线程池,初始化线程个数为5
ExecutorService es= Executors.newFixedThreadPool(5);
//定义集合保存结果集
List<String> list=new ArrayList();
for(int i=0;i<5;i++){//循环遍历5个线程
//实例化线程对象返回线程所在的name值
C1 c1=new C1("我的名字:"+Thread.currentThread().getName()+" ");//起名字 main
//Future表示异步对象计算的结果
Future<String> future=es.submit(c1);
try {//返回可能出现的异常
list.add(future.get());//取出的值存入集合
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
es.shutdown();//调用方法,使线程池立即变为shutdown状态
//不能再向线程池中添加任何任务
list.forEach(c->System.out.print(c+" "));//遍历结果集
}
}
class C1 implements Callable<String> {//定义内部类C1 实现Callable接口
private String name;//定义变量name
public C1(String name){//定义构造器返回name的值
this.name=name;
}
@Override
public String call() throws Exception {//抛出异常
return this.name;//返回name的值
}
}
输出结果:
实现接口要优于继承Thread
因为Java不支持多继承,所以继承了Thread类就无法继承其他类,但可以实现多个接口
继承Thread容易造成开销过大
三种创建线程的区别
1.将线程的代码和数据分开,形成清晰的模型
2.可以继承其他类
1.不能继承其他类
2.编写简单,可以直接操作线程
1.规定方法不同,Callable使用call方法,而Runnable使用run方法
2.Callable可以有返回值,Runnable没有返回值
3.call方法可以抛出异常,而run方法不能抛出异常
线程中的主要方法
Executor
分类有三种:
CachedThreadPool:一个任务创建一个线程
FixedThreadPool:创建固定大小线程
SingleThreadExecutor:创建大小为1的固定线程
sleep
注意:线程中抛出的异常需要在本地进行处理
wait
notify():唤醒当前对象的等待状态
notifyAll():唤醒所有线程
1.sleep方法属于Thread类,wait属于Object类
2.sleep方法暂停执行指定的时间,让出CPU给其他线程,在指定时间后会自动恢复运行状态。
3.调用sleep方法过程中,线程不会释放锁,而wait会释放当前锁。
5.使用wait方法时,只有调用notify方法后,该线程才会获取锁,进入运行状态。
yield
作用:让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会从而进行适当的轮转执行。
缺点:实际中可能无法达到让步目的,让步的线程还有可能被线程调度程序再次选中。
锁
定义:
为了保障多线程中的数据一致性,通常需要在对象或方法之前加锁。
如果有其他线程也需要使用该对象或方法时,则首先需要获得锁。
如果某个线程发现锁正在被其他线程使用,则会进入阻塞队列等待锁的释放,直到其他线程执行并释放锁后,该线程才能继续进行操作。
可以保证同一时刻只有一个线程拥有锁并修改对象,保证数据的安全。
乐观锁:
在读取数据时不加锁,而在更新操作时才加锁。
通常采用在写时读取当前版本号然后加锁
比较当前版本号与上一次版本号,如果一致则进行更新操作,否则将重复进行读写,比较操作
悲观锁:
在读写数据时都要加锁,在别的线程对数据进行操作时会被阻塞,等待直到拿到锁。
自旋锁:
如果持有锁的线程能在自旋等待时间内释放锁资源,则会进入阻塞,挂起状态。当超过最大时间后线程会退出自旋模式并释放锁。
优点:可以减少CPU上下文切换对于锁的占用时间,同时提高性能。
缺点:不适合在系统具有复杂依赖的情况下使用,会造成线程长时间获取不到锁资源,引发CPU浪费。
Synchronized
Synchronized属于独占式悲观锁,可重入锁
1.同步一个代码块
2.同步一个方法
3.同步一个类
4.同步一个静态方法
Synchronized在修饰对象,方法,代码块时,同一时刻只能有一个线程进行访问
从获取资源的公平性角度可以分为公平锁和非公平锁
从是否共享资源的角度可以分为共享锁和独占锁
从锁的状态角度可以分为偏向锁,轻量级所和重量级锁