title: java多线程
date: 2020-09-27 21:31:03
tags:
一、常用的创建多线程的两种方式
方式一:继承Thread类
1、创建一个继承于Thread类的子类
2、重写Thread类的run()--->即为此线程的操作
3、创建Thread类的子类的对象
4、通过此对象调用start():①启动当前线程。②调用当前线程的run()方法。
package demon;
public class Thread1 extends Thread{
public static int a = 2;
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread1 thread2 = new Thread1();
Thread1 thread3 = new Thread1();
thread1.start();
thread2.start();
thread3.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+a+"执行了");
}
}
注:①启动线程,必须调用start()方法,再调run()方法。②如果想要多个线程,必须创建多个子类对象。
方式二:实现Runnable接口
** 1、创建一个实现了Runnable接口的类。
** 2、实现类去实现Runnable中的抽象方法run()。
** 3、创建实现类对象
**4、将此对象作为参数传递到Thread类的构造器,创建Thread类的对象
**5、通过Thread类的对象调用start()。
public class Thread2 implements Runnable{
public int a = 2;
public static void main(String[] args) {
Thread2 thread1 = new Thread2();
new Thread(thread1).start();
new Thread(thread1).start();
new Thread(thread1).start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+a+"执行了");
}
}
两种方式比较
* 开发中一般选用Runnable
原因:1、实现的方式突破了单继承的局限性。
2、实现的方式更适合处理多线程共享数据的情况。因为我们是把子类放在Thread类 中,每个子类都已经包含了属性值之类的。如果Thread类的话需要将这些信息设static。
*联系:Thread实现了Runnable接口
*相同点;二者要想启动线程,都是调用Thread类的start()方法。
二、线程的生命周期
注:sleep(),join()不会释放锁,wait()会释放锁
三、同步机制
一、同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:1、操作共享数据的代码。
2、同步监视器:(锁),任何一个类都可以充当锁,每个线程的锁必须是同一个。锁必须是 唯一的。
3、在实现Runnable接口的方式中,考虑用**this**充当锁,因为这个this表示的实现类,我们调用start方法时,是把这个实现类放到new Thread()中,我们只创建了一个实现类对象。
在继承Thread类的方式中,慎用this充当锁,可以用当前类来充当。
二、同步方法
1、使用情况:如果操作共享数据的代码完整的声明在一个方法中,我们可以将这个方法声明为同步的。
2、总结:①同步方法本身其实也有锁,只是没有显示声明什么锁。
②非静态的同步方法,同步监视器:this
静态的同步方法,同步监视器:当前类本身
三、lock锁
使用方式
/*
* 使用ReentrantLock类实现同步
* */
class MyReenrantLock implements Runnable{
//向上转型
private Lock lock = new ReentrantLock();
public void run() {
//上锁
lock.lock();
for(int i = 0; i < 5; i++) {
System.out.println("当前线程名: "+ Thread.currentThread().getName()+" ,i = "+i);
}
//释放锁
lock.unlock();
}
}
public class MyLock {
public static void main(String[] args) {
MyReenrantLock myReenrantLock = new MyReenrantLock();
Thread thread1 = new Thread(myReenrantLock);
Thread thread2 = new Thread(myReenrantLock);
Thread thread3 = new Thread(myReenrantLock);
thread1.start();
thread2.start();
thread3.start();
}
}
1、synchronized与Lock的异同:
同:二者都可以解决线程安全问题
异:synchronized机制在执行完相应的同步代码块以后,自动的释放同步监视器
Lock需要收欧东的启动同步(Lock()),同时结束同步也需要手动的事项(unlock())
2、使用顺序:
Lock—》同步代码块(已经进入了方法体,分配了相应资源)—》同步方法(在方法体外)
四、同步的利弊
同步的方式,界限了线程的安全问题---》利
操作同步代码块时,只能一个线程参与,其他线程等待,还是相当于一个单线程过程--》弊
五、同步机制的应用
一、单例模式
懒汉式
class Bank{
//变量随着类的加载而加载,而不着急创建对象,需要的时候在创建
private static Bank instance = null;
//私有化构造器
private Bank(){}
//提供一个公有方法去创建对象,运用同步机制,避免创建多个对象,并返回
public static Bank getInstance(){
if(instance=null){
synchronized(Bank.class){
if(instance==null){//如果没有这个就会创建多个对象
instance = new Bank();
}
}
}
return instance;
}
}
饿汉式
class Bank{
//变量随着类的加载而加载,直接创建对象
private static Bank instance = new Bank();
//私有化构造器
private Bank(){}
//提供一个公有方法去创建对象,运用同步机制,避免创建多个对象,并返回
public synchronized static Bank getInstance(){
return instance;
}
}
二、死锁问题
1、理解:就是有多个线程分别占着对方需要的资源,都在等待对方释放资源,然后就gg了。
2、说明:出现死锁时,不会出现异常,提示,所有线程都处于阻塞的状态,不会体质。我们使用同步要避免死锁。下面举例:
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);//增大死锁的几率,在睡眠的时候另一个线程很有可能需要 这个锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);//增大死锁的几率,在睡眠的时候另一个线程很有可能需要 这个锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
六、线程通信(多个线程进行具有一定联系的操作)
1、线程通信涉及的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放锁。
- notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个被wait就唤醒优先级最高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
2、说明
* 1、这三个方法必须使用在同步代码块或同步方法中,这样才能达到线程通信,
* 2、这三个方法的调用者必须是this或者当前类,不能像之前那样是obj,那样就不能调用这三个方法了。
3、sleep()和wait()异同
- 1、相同点:一旦执行方法,都可以使当前线程进入阻塞状态。
- 2、不同点: ①两个方法声明的位置不同:Thread类声明sleep(),Object类声明wait()
②调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块中。
③sleep()不会释放锁,wait()会释放锁
4、释放与不释放锁的操作
1、释放锁的操作
- 当前线程同步代码块,同步方法结束或遇到break,return结束
- 出现了Error或者Exception,导致异常结束
当前线程在同步代码块、方法中执行了线程对象的wait()方法,线程暂停,释放锁。
2、不会释放所得操作
* 当前线程调用Thread.slepp() Thread.yield()方法,暂停当前线程的执行
* 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。(应尽量避免使用suspend()和resume()来控制线程)。
5、线程通信的具体实例(生产者消费者模式)
package com.atguigu.java2;
/**
* 线程通信的应用:经典例题:生产者/消费者问题
*
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
* 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
* 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1. 是否是多线程问题?是,生产者线程,消费者线程
* 2. 是否有共享数据?是,店员(或产品)
* 3. 如何解决线程的安全问题?同步机制,有三种方法
* 4. 是否涉及线程的通信?是
*
* @author shkstart
* @create 2019-02-15 下午 4:48
*/
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
面试题
谈谈Synchronized和lock
synchronized是java关键字,用它来修饰一个方法或者一个代码块的时候能够保证在同一时刻最多只有一个线程执行该代码块。他是内置的语言实现。
Lock是一个接口,1. synchronized发生异常的时候会自动释放线程占有的锁,所以不会导致死锁的发生,Lock发生异常的时候,如果没有主动通过unLock()去释放锁,则很可能造成死锁,因此使用Lock需要在finally块中释放锁; 2. Lock可以让等待锁的线程发生响应中断,synchronized却办不到。使用synchronized时,等待的线程会一直等待下去,不会响应中断;通过Lock可以知道有没有成功获取锁,通过Lock可以知道有没有获取到锁,而synchronized却办不到。
介绍一下volatile
volatile是用来保证有序性和可见性的。
可见性:就是有两个线程A,B,对于共享变量,他们都会先将共享变量从主内存读到自己的工作内存中去,假设A做了+1的操作,还没刷新到主内存中去那么B读到的依然还是主内存中的原始值,用了volatile就可以强制刷新到主内存去,这样B读到的值就是对的。
有序性:通过插入内存屏障来保证。执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行,如果不是的话就有可能指令重排出现下面的情况,给a加了volatile就不会出现
public class TestVolatile{
int a = 1;
boolean status = false;
//状态切换为true
public void changeStatus{
a = 2; //1
status = true; //2
}
//若状态为true,则为running
public void run(){
if(status){ //3
int b = a + 1; //4
System.out.println(b);
}
}
}
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
创建线程有几种方式?哪一种好,为什么
有三种:继承Thread类,实现Runnable接口,执行Executor框架创建线程池
实现Runnable接口比较好,因为java能多继承,而且实现接口他是把一个实现接口的对象放进去,天生就有了共享变量,使用继承的话还要把对象的变量设成static。
多线程回调是什么意思
客户程序C调用服务程序S中的某个方法A,然后S在某个时候又反过来调用C中的某个方法B,这个B就叫回调方法
启动线程有哪几种方式
三种
(1)继承Thread类
定义Thread类的子类
创建Thread实例,创建线程对象
调用start方法
(2)实现Runnable接口
定义实现接口的类,重写run方法
创建实例,把这个实例作业Thread的target来创建Thread对象,这才是真正的线程对象
调用线程对象的start方法
(3)通过Callable和Future创建线程
创建Callable接口的实现类,并实现call方法,这个相当于run()方法
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该Future对象封装了call()方法的返回值
使用Future对象作为Thread对象的target创建并启动新线程
调用FutureTask对象的get()方法来获得子线程执行结束后返回值
创建Callable接口的实现类,并实现call方法,这个相当于run()方法
public class aa implements Callable<Integer> {
public static void main(String[] args) {
创建Callable实现类的实例,
aa aa = new aa();
用FutureTask类来包装Callable对象,该Future对象封装了call()方法的返回值
FutureTask<Integer> task = new FutureTask<>(aa);
for (int i = 0; i < 10; i++) {
System.out.println("当前线程"+Thread.currentThread().getName());
if(i==5){
使用Future对象作为Thread对象的target创建并启动新线程
new Thread(task,"有返回值的线程").start();
}
}
try {
调用FutureTask对象的get()方法来获得子线程执行结束后返回值
Integer integer = task.get();
System.out.println("线程的返回值"+integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
for (int i = 0; i < 3; i++) {
System.out.println("---->"+i);
}
return new Integer(1);
}
}
java中有几种线程池,描述一下线程池的实现过程
newFixThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大树,则将提交的任务存放到池队列中
newCachedThreadPool:创建一个可缓存的线程池,这个线程池的特点是: 1、工作线程的创建数量几乎没有限制(Integer.MAX_VALUE),这样可以灵活的往线程池中中添加线程。 2、 如果长时间没有往线程池中哦你提交任务,即如果工作线程空闲了指定时间(默认一分钟),则该工作线程将自动终止。终止后,如果你有重新提交了新的任务,则线程池重新创建新的线程
newSingleThreadExecutor:创建一个单线程话的Executor,之窗键一个工作线程来执行任务,如果这个线程异常结束,会有另外一个取代它,保证顺序执行。单工作线程最大的特点就是顺序的执行各个任务,并且在任一给定的时间是不会有多个线程活动的
newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的执行任务,类似Timer。
说明一下你对AQS的理解
AQS其实就是一个可以给我们实现锁的框架
内部实现的关键是:先进先出的队列,state状态
定了内部类ConditionObject
拥有两种线程模式:独占模式和共享模式
在Lock包中的相关锁都是基于AQK来构建,一般叫AQS叫同步器。
多线程的i++线程安全吗?
不安全,i++不是原子操作,i++分别是读取i的值,对i值+1,再赋值给i++,执行期中的任何一步都有肯呢个被其他线程抢占。
请说明锁(Lock)和同步(synchronized)的区别
用法上:同步既可以加载方法上也可以加载代码块上,lock需要显示的指定其实位置和终止位置
同步是托管给jvm执行的,lock的锁定是通过代码是先的,它有比同步更精确的线程语义
性能上:在竞争不是很激烈的时候同步的性能由于Lock,竞争激烈的时候同步的性能下降的非常快,而Lock基本不变。
锁机制不同:同步获取锁和释放锁的方式都是在块结构中,当获取多个锁的时候,必须以相反的顺序释放,并且是自动解锁。而Lock则需要开发人员手动释放,并且必须在finally中释放,否则会引起死锁。
synchronized的可重入怎么实现
每个锁关联一个线程持有者和一个计数器。当即暑期为0的时候表示该锁没有被任何线程持有,那么任何线程都可以获取该锁而调用相应的方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器为1。,此时其他线程必须等待。而持有锁的线程如果再次请求这个锁,就可以拿到这个锁,计数器+1.当先成推出一个同步方法或者同步块的时候,计数器会递减,如果计数器为0则释放该锁。
讲一下非公平锁和公平锁在reetrantlock里的实现过程是怎样的
如果锁是一个公平的,那么锁的获取顺序应该符合请求的绝对时间顺序,FIFO。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。