多线程
进程与线程的区别
进程
一个应用程序(1个进程是一个软件),是系统进行资源分配和调用的独立单位。
线程
一个进程中的执行场景/执行单元,是进程中的单个顺序控制流,是一条执行路径。
一个进程可以包括多个线程。
多线程的实现方式
1.继承Thread类
1.创建自定义类 继承 Thread
2.重写run方法,并再run方法中实现线程中的执行逻辑
3.创建自定义类的对象 再执行其start方法
public class CreateThread extends Thread {
// run方法中编写 多线程需要执行的代码
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
public static void main(String[] args) {
// 1.创建一个线程
CreateThread createThread = new CreateThread();
// 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
createThread.start();
}
}
2.实现Runnable接口
1.创建自定义类 实现其接口
2.重写run方法
3.创建 Thread对象,并将自定义类的对象传入 Thread构造方法中
4.通过Thread对象调用start方法
public class CreateRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
public static void main(String[] args) {
// 1.创建一个线程
CreateRunnable createThread = new CreateRunnable();
// 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
Thread thread = new Thread(createThread);
thread.start();
}
}
Thread与Runnable比较:第一种通过继承Thread重写run方法实现的,受Java单继承的限制,无法继承其他类,而第二种方法是实现Runnable接口,可以继承其他类,多个线程共享同一个变量时更加方便。
3.实现Callable接口
1.创建自定义类实现其接口
2.重写其call方法 注意:对于call方法具有返回值 其返回值类型可以再Callable接口上给定其泛型
3.构建线程池对象,使用submit对当前自定义类对象进行提交执行
4.通过shutdown对当前线程进行关闭操作
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
4.匿名内部类
1.new Thread(){重写其run方法} 获取到对象并对其进行start
2.new Thread (new Runable 匿名内部类对象传入)之后进行start启动
public class CreateRunnable {
public static void main(String[] args) {
//创建多线程创建开始
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
});
thread.start();
}
}
Runnable接口与Callable接口的联系和区别:
联系:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
区别:Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
Thread类的构造方法
public Thread(ThreadGroup group, Runnable target, String name)
ThreadGroup可以指定当前线程属于哪个线程组,对于线程组的使用,创建ThreadGroup()对象,括号内传入线程组的名称,在创建线程时传入该名称即可把线程归属到该名称的线程组内。
Runnable可以传入一个实现了Runnable接口的对象,为这个对象创建线程
String name可以设置当前线程的名称
多线程常用方法
-
获取线程的名称
//这段代码被某个线程执行,返回的就是对应线程的名称 Thread.currentThread().getName()
对象名.getName() 例如有一个Student对象,为这个Student对象创建线程,在创建线程时可以传入线程名 Student student = new Student(); Thread thread = new Thread(student,"1"); thread.getName()//可以获取到当前线程的线程名
-
设置线程名
1.new Thread(Runnable接口实现对象,"名称")
2. 创建一个继承了Thread类的对象,通过其构造方法 进行传递,在构造方法中使用 super(线程名变量)
public Student(String name) {
super(name);
}
3.使用Thread对象.setName添加
- 启动线程
3.start 用于启动一个线程,执行run方法中的逻辑
可以使用Thread对象名.start()的方式调用
- 线程睡眠阻塞
.sleep 给定一个倒计时时间阻塞当前线程
因为sleep是静态方法,故可以使用Thread类名.方法名的形式调用
- join
5.join 当给定的线程使用join之后,该线程会一直占用当前CPU的执行权,直到其线程死亡,释放CPU的执行权。给定一个时间参数,可以限制当前线程的执行时间,后续和其他线程竞争执行权。
- yield
6.yield 线程礼让,让同级的或优先级较高的线程先运行,非常短的时间当程序执行到该方法时,会将当前线程的CPU执行权进行释放,再进行下一轮的CPU执行权的竞争
- stop
7.stop 当被调用时,当前线程执行结束,并不影响其他线程执行
- interrupt
8.interrupt 对于该方法执行后,不会像stop方法立即终止当前线程,而是会等待一小段时间后,再去停止线程
9.setDaemon 对于守护线程,也称为后台线程,当该线程执行完成后,整个应用程序退出,若其他线程未执行完成,抛出一个异常
- 设置线程的优先级
thread.getpriority():获取线程的优先级 默认5
thread.setPriority():设置线程的优先级,值越大优先级越高
设置线程的优先级后只能保证谁优先执行并不能保证谁先执行完成。
wait与sleep区别
- sleep()方法,是属于Thread类中的。
- wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是没有释放锁
- wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
多个线程之间的问题
-
对多个线程进行操作时,由于多个线程互相抢占cpu资源,可能导致某个线程在执行过程中被其他线程打断。
-
当多个线程操做同一个变量时,由于多个线程之间存在抢占,可能导致一个线程的一次执行还没结束时,被另一个线程抢占,可能会导致对这个变量的操做结果出现错误。
死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程),简单来说就是多个线程之间互相竞争cpu资源,各自持有对方的资源,互相等待。
线程锁
syncronized锁
syncronized:同步锁,可以修饰代码块,也可以成员修饰方法,静态方法。对于syncronized修饰run方法后,只有一个线程在运行。
- 修饰成员方法:成员方法 对当前对象进行加锁操作,优点是可以直接对当前对象进行加锁,缺点是要求当前对象只能有一个 比较清晰简介。
- 修饰静态方法:对当前方法的类进行加锁,类加锁后其类产生的对象也会被加锁 ,静态方法的类进行加锁,其对象可以有很多个 。
- 修饰同步代码块:可以指定具体的对象进行加锁操作,可以指定任意一个对象进行加锁,需要对对象进行传递,并且对象只能有一个 。
同步代码块:
synchronized(){}
()中可以传入一个对象,对对象进行加锁操做,被加 锁的对象要有唯一性,
{}写入代码块
方式1:()使用this传入自身类的对象
方式2:在本类创建Object对象,在()传入一个Object对象
方式3:创建一个Object对象的变量,把变量写入构造函数
Lock锁
Lock锁是java.util.concurrent.locks中的一个接口。Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
使用lock和Unlock方法 获取 Lock类的子类之后,对对象调用lock方法,当前lock和unlock之间的程序会被加锁 。
使用方法:创建new ReentrantLock()对象,用对象名.lock()或对象名.unlock()的方式加锁,或解锁。
private Lock lock = new ReentrantLock();
//使用完毕释放后其他线程才能获取锁
public void lockTest(Thread thread) {
lock.lock();//获取锁
try {
} catch (Exception e) {
}finally {
lock.unlock(); //释放锁
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private Lock lock = new ReentrantLock();
//使用完毕释放后其他线程才能获取锁
public void lockTest(Thread thread) {
lock.lock();//获取锁
try {
System.out.println("线程" + thread.getName() + "获取锁");
} catch (Exception e) {
System.out.println("线程" + thread.getName() + "发生了异常");
} finally {
System.out.println("线程" + thread.getName() + "执行完毕释放锁");
lock.unlock(); //释放锁
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
Thread thread1 = new Thread(new Runnable() {
public void run() {
lockTest.lockTest(Thread.currentThread());
}
}, "线程一");
Thread thread2 = new Thread(new Runnable() {
public void run() {
lockTest.lockTest(Thread.currentThread());
}
}, "线程二");
// 启动2个线程
thread2.start();
thread1.start();
}
}
synchronized与Lock的区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,自动放弃的方法tryLock(),具有更完善的错误恢复机制。
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
线程通信
什么是多线程之间的通讯
在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。
wait、notify和方法
- 因为涉及到对象锁,他们必须都放在synchronized中来使用. wait、notify一定要在synchronized里面进行使用。
- wait() :使当前线程处于等待状态,执行到该代码时当前线程处于等待状态,需要其他线程来唤醒当前线程的执行。
- notify() :唤醒因锁池中的线程,使之运行,nofity()可以唤醒一个同步锁锁住的同一个对象或同一个类。
//输入类
class InputThread extends Thread {
private User user;
public InputThread(User user) {
this.user = user;
}
public void run() {
int count = 0;
while (true) {
synchronized (user) {
if (user.sign == true) {
try {
// 当前线程变为等待,但是可以释放锁
user.wait();
} catch (Exception e) {
}
}
if (count % 2 == 0) {
user.name = "张三";
user.sex = "男";
} else {
user.name = "小紅";
user.sex = "女";
}
count += 1;
user.sign = true;
// 唤醒当前线程
user.notify();
}
}
}
}
//输出类
class OutThread extends Thread {
private User user;
public OutThread(User user) {
this.user = user;
}
public void run() {
while (true) {
synchronized (user) {
if (user.sign==false) {
try {
// 当前线程变为等待,但是可以释放锁
user.wait();
} catch (Exception e) {
}
}
System.out.println(user.name + "====" + user.sex);
user.sign = false;
// 唤醒当前线程
user.notify();
}
}
}
}
多线程的生命周期
- 创建对象时,该对象处于新建状态
- 当使用start方法启动一个线程时,那么该线程处于就绪状态,可以竞争获取CPU的执行时间片段
- 当获取到执行权限时,就处于运行状态,如果run方法执行结束,之后线程处于死亡状态
- 当获取到执行权限时,就处于运行状态,运行过程中如果遇到sleep那么就处于睡眠阻塞状态,睡眠状态结束后,再进入就绪状态
- 当获取到执行权限时,就处于运行状态,运行过程中如果遇到wait那么就处于等待阻塞,那么后续需要相同锁的其他线程对其进行唤醒操作 唤醒操作需要使用 notify函数,再进入就绪状态
- 当获取到执行权限时,就处于运行状态,运行过程中如果遇到synchronized关键字,并且加锁的内容没有释放锁,那么当前处于同步阻塞状态等待其他线程对当前锁进行释放,之后再处于就绪状态
线程池
ExecutorService.newFixedThreadPool( )传入int类型参数表示开启几个空线程。
线程池对象名.submit(),传入要开启线程的对象,创建指定对象的多个线程,可以执行线程池类对象的run()方法。
线程池名.shutdown():所有的线程执行结束后,对线程进行关闭。
定时器
指定之间执行某个应用程序
Timer: 创建对象后可以通过schedule方法设置一个定时器
.schedule()方法可以传入三个参数,
TimerTask对象
delay:第一次延迟执行的时间
以及period周期:传入参数表示多久运行一次。
TimerTask:创建一个定时器的执行任务,需要重写器run方法,在定时器触发时,会执行run方法中的逻辑。