多线程基础
线程的基本概念
线程的使用
创建线程的两种方式:
- 继承Thread类,重写run()方法
- 实现Runnable接口,重写run()方法
第一种情况继承Thread创建线程
public class Thread1 {
public static void main(String[] args) throws InterruptedException {
//在主线程中创建对象,并开启一个子线程
Cat cat = new Cat();
cat.start();//开启线程
System.out.println("主线程继续执行");
System.out.println("主线程名字: " + Thread.currentThread().getName());//main
//打印60次i
for(int i = 0; i < 60; i++){
System.out.println("主线程" + i);
Thread.sleep(1000);
}
}
}
//通过继承Thread类来实现线程
class Cat extends Thread{
@Override
public void run() {
//打印当前线程名字
System.out.println("当前线程名字: " + Thread.currentThread().getName());//Thread0
int count = 0;
//打印,猫喵喵,每个1秒打印一次
while(true){
System.out.println("猫喵喵" + (++count));
//让线程睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当打印801次后退出循环,线程结束
if(count == 80){
break;
}
}
}
}
当我们的主线程执行到cat.start()就会创建一个新的线程,执行Cat类中重写的run方法内容,主方法后面代码不阻塞继续执行,两个线程并发的执行,当两个线程都执行完成退出以后,整个进程就执行完成退出。默认的主方法线程名为:main,新线程的名字是Thread0。我们可以使用Jconsole监控线程。
为什么我们创建线程不是执行run方法而是执行start方法
如果我们直接调用run,那么并不会开启一个新的线程,只会想普通方法一样执行,在start方法里面,通过调用start0方法来真正的执行多线程,这个start0是一个本地的方法,由JVM来调用,当我们执行start方法以后,线程并不是立即执行,而是变成可运行的状态,等待CPU调度,只有线程获得了CPU调度的时候,线程才会真正执行。
第二种情况实现Runnable接口重写run方法来实现多线程
在Java中只能单继承,如果一个类继承了一个父类也想实现多线程,就不能通过继承Thread来线程,只能通过实现Runnable接口来实现多线程。
public class Thread2 {
public static void main(String[] args) {
Tiger tiger = new Tiger();
//使用静态代理模式开启线程
Thread thread = new Thread(tiger);
thread.start();//开启线程
}
}
class Tiger implements Runnable{//实现Runnable接口来实现线程
@Override
public void run() {
int count = 0;
while(true){
System.out.println("老虎嗷嗷叫:" + (++count) + "线程名:" + Thread.currentThread().getName());
//线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 10){
break;//执行10次就停止
}
}
}
}
在实现Runnable接口中,由于Runnable接口中并没有start方法,所以我们想要启动多线程,必须借助Thread类来实现,这里使用到静态代理模式。在Thread类内部会有一个Runnable属性,通过创建Thread线程的时候我们可以将其传入赋值,我们可以看Thread源码:
当我们调用start执行多线程的时候执行的run会通过动态绑定机制,执行我们传入的对象的run方法。在这里就是由Thread类帮我们的对象去实现多线程。
Thread和Runnable实现线程方式区别
本质上面并没有什么区别,只不过Java不支持多继承,使用实现Runnable接口更适合多线程资源共享。
创建线程实现Callable接口,这种方式可以有返回值,也可以抛出异常
class AAA implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
for(int i = 0; i < 10; i++){
System.out.println("hello");
}
return true;
}
}
//1.创建对象
AAA aaa = new AAA();
//开启服务,线程池大小为1
ExecutorService executorService = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> r1 = executorService.submit(aaa);
//获取结果
Boolean rs1 = r1.get();
//关闭服务
executorService.shutdownNow();
线程中止
- 当线程完成任务以后后,会自动退出
- 还可以使用变量来控制run方法退出的方式来让线程终止,即通知方式。
public class Thread5 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
//主线程10秒后结束T线程运行
System.out.println("主线程在10秒后结束T线程运行");
Thread.sleep(1000*10);
t.setFlag(false);
}
}
class T extends Thread{
private boolean flag = true;
@Override
public void run() {
int count = 0;
while(flag){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T 线程在运行...." + (++count));
}
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
线程常用方法
注意事项:
-
start底层会创建新线程,调用run(),run()就是一个简单的方法,不会启动新的线程。
-
线程优先级范围 1-10
-
interrupt,中断线程,并没有结束线程,一般用于结束正在休眠的线程
-
sleep,是一个静态方法,使当前线程睡眠
setName() 设置线程名字
getName() 获取线程名字
start() 线程开始
run() 调用线程的run方法
setPriority() 设置线程优先级
getPriority() 获取线程优先级
sleep() 线程睡眠
interrupt() 中断线程
yield() 礼让线程,让出CPU,让其它线程执行,但礼让时间不确定,礼让也不一定成功
join() 线程插队,线程一旦插队成功,肯定先执行完插队的线程的所有任务,再执行其它线程。
public class Thread6 {
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();//启动子线程
for(int i = 0; i < 20; i++){
if(i == 5){
b.join();//让b线程插队
}
System.out.println("hi");
Thread.sleep(1000);
}
}
}
class B extends Thread{
@Override
public void run() {
int count = 0;
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if((++count) == 20){
break;
}
}
}
}
用户线程和守护线程
用户线程也叫工作线程,当线程的任务执行完成或者被通知结束线程
守护线程,一般是为工作线程服务的,当所有的用户线程结束,守护线程自然结束
常见的守护线程:垃圾回收机制
setDaemon(true) 可以将一个方法设置成守护线程
线程的生命周期
线程同步
在多线程编程中,一些敏感的数据不允许被多个线程同时访问,这时就要使用线程同步访问机制,保证线程在任何时刻最多只有一个线程访问,以保证数据的完整性。也可以理解为当一个线程对某个内存进行操作的时候,其它地址都不允许对这个内存地址进行访问,直达该线程对这个内存地址访问操作完成,其它线程才可以对其进行操作。
同步具体方法加锁Synchronized,有两种方式,同步代码块,同步方法。
使用同步解决售票问题:
public class Thread4 {
public static void main(String[] args) {
//模式售票
ThreadTest2 threadTest2 = new ThreadTest2();
new Thread(threadTest2).start();
new Thread(threadTest2).start();
new Thread(threadTest2).start();
}
}
//通过继承Runnable
class ThreadTest2 implements Runnable{
private int num = 100;
private boolean flag = true;
@Override
public void run() {
while (flag){
m1();
}
}
public synchronized void m1(){//同步方法
if(num <= 0){
System.out.println("售票结束");
flag = false;
return;
}
//模拟等待
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩:" + (--num));
}
}
互斥锁
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作完整性
- 每个对象都有一个称为互斥锁的标记,这个标记用于保证,在任何时刻只有一个线程能够访问该对象
- 关键词synchronized与对象的互斥锁联系,当某个对象使用synchronized修饰的时候,表示,在任何时刻只有一个线程能够访问该对象
- 同步会导致程序的性能降低
- 同步方法(非静态)的锁可以是this,也可以是其它对象,要求是一个对象
- 静态同步方法的锁是当前类本身
必须保证多线程的锁对象是同一个
Lock
class BBB implements Runnable{
private ReentrantLock lock = new ReentrantLock();
private int num = 10;
@Override
public void run() {
while(true){
try{
lock.lock();//加锁
if(num <= 0){
return;
}
//模拟延时
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
} finally {
//释放锁
lock.unlock();
}
}
}
}
BBB bbb = new BBB();
new Thread(bbb,"线程1").start();
new Thread(bbb,"线程二").start();
new Thread(bbb,"线程三").start();
死锁
多个线程都想占用对方的锁资源,但不肯想让,导致死锁。
模拟死锁:
public class Thread9 {
public static void main(String[] args) {
AA a = new AA(true);
a.setName("线程A");
AA b = new AA(false);
b.setName("线程B");
a.start();
b.start();
}
}
class AA extends Thread{
private static Object obj1 = new Object();//对象1
private static Object obj2 = new Object();//对象2;
private boolean flag;
public AA(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
//进行同步获取obj1的锁
synchronized (obj1){
System.out.println(Thread.currentThread().getName() + "获得obj1资源");
//进行同步获取对象obj2的锁
synchronized (obj2){
System.out.println(Thread.currentThread().getName() + "获取obj2资源");
}
}
}else{
//进行同步获取对象obj2的锁
synchronized (obj2){
System.out.println(Thread.currentThread().getName() + "获取obj2资源");
//进行同步获取obj1的锁
synchronized (obj1){
System.out.println(Thread.currentThread().getName() + "获得obj1资源");
}
}
}
}
}
- 互斥条件,一个资源每次只能被一个进程使用
- 请求与保持条件,一个进程因请求资源阻塞时,对已获得的资源保持不放
- 不可剥夺条件,进程已经获得的资源,在没有使用完之前,不能强行剥夺
- 循环等待条件,若干进程之间形成一种头尾相接的循环等待资源关系
避免死锁
破坏上面任意一个或者多个条件即可