Java中多线程同步问题、生产者与消费者、守护线程和volatile关键字(附带相关面试题)

1.多线程同步问题(关键字Synchronized)

问题:多线程访问同一个资源时候可能就会出现资源完整性的问题

所以引入关键字synchronized(同步)

  • synchronized关键字的作用机制是给对象加锁,并为每个线程提供了一个计数器,初始值为0。当第一个线程获得锁时,计数器变为1,其他线程被阻塞。当第一个线程执行完代码并释放锁时,计数器归零,意味着资源可用,所有被阻塞的线程将恢复执行。
  • 一个通俗的比喻是厕所的使用情况。假设只有一个厕所位置但有很多人需要使用。当第一个人进入厕所并锁上门时,其他人不得不在外面等待。当第一个人使用完毕并打开门锁时,表示厕所空闲可用,所有等待的人可以继续使用。

关于synchronized有两种用法

1.设置同步代码块

public void method() {
    synchronized (obj) {
        // 同步代码块
    }
}

意味着只有同步代码块内部的代码需要同步,其他操作无需同步

2.设置同步代码方法

public synchronized void method() {
    // 同步代码块
}

这个方法就是整个方法内的代码都是同步的

注意:在生产案例中不要随意使用同步方法,因为一旦同步,整个程序的运行效率就会非常低,比如10个学生想要去学校上厕所,那么最好的操作就是让10个学生一起先到学校再同步操作上厕所,而不是10个学生其中某一个去学校上完厕所,其他学生才去学校上厕所


关于同步案例:(多个售票员售卖固定数量的票)

在上一篇文章的代码中就实现到这一步

package Example1401;
 
 
class MyThread extends Thread{
//    设置只有100张票
    private int ticket = 100;
    @Override
    public void run() {
        while (ticket>0){
            try {
//                内部数字单位为毫秒 1000毫秒就是1秒
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"卖出第"+(ticket--)+"张票");
        }
    }
}
 
public class javaDemo {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        m1.setName("售票员1");
        m2.setName("售票员2");
        m3.setName("售票员3");
        m1.start();
        m2.start();
        m3.start();
    }
}

这一步实现了售票员之间的售卖间隔,就不会一下子就把所有票卖光。

现在通过同步代码块方法实现同步:

package ExampleThread;

import java.util.Random;

class Mythread implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
//           当没买完票则开始卖票
//                实现同步
            synchronized (this) {
//                判断是否有票
                if (ticket > 0) {
                    try {
//                        设置随机售卖出去的时间间隔
                        Thread.sleep(1000);
//                        输出售卖信息
                        System.out.println(Thread.currentThread().getName() + "卖第" + ticket-- + "张票");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (ticket <= 0) {
                System.out.println("票已经全部卖光了");
                break;
            }
//            线程礼让,让其他线程也有机会售出票
            Thread.yield();
        }
    }
}

public class test {
    public static void main(String[] args) {
        Mythread task = new Mythread();
        new Thread(task, "售票员A").start();
        new Thread(task, "售票员B").start();
        new Thread(task, "售票员C").start();

    }
}

由于票太多所以为了方便显示就改成5了

 

面试题: Synchronized 用过吗,其原理是什么?

(1)可重入性

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;

    可重入的好处:
    可以避免死锁;
    可以让我们更好的封装代码;

synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

(2)不可中断性

    一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;
    synchronized 属于不可被中断;
    Lock lock方法是不可中断的;
    Lock tryLock方法是可中断的;

 面试题:为什么说 Synchronized 是非公平锁?

当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。

面试题:为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?

Synchronized的并发策略是悲观的,不管是否产生竞争,任何数据的操作都必须加锁。

乐观锁的核心是CAS,CAS包括内存值、预期值、新值,只有当内存值等于预期值时,才会将内存值修改为新值。


2. Object线程的等待与唤醒方法

注意:

这些方法必须在同步块或同步方法中使用,因为它们会改变对象的内部锁状态。调用wait()方法将释放当前线程持有的对象锁,并使线程进入等待状态。而调用notify()notifyAll()方法会唤醒等待在该对象上的线程,并将其重新放入可运行状态。

案例要求:设置一个图书类,一个图书管理员可以放图书的书名和作者,一个读者可以看图书的书名和作者

package Example;
class Book{
//    封装属性
    private String author;
    private String bookName;
//    设置获取属性
    public void setAuthor(String author) {
        this.author = author;
    }
    public void setBookName(String bookName) {
        this.bookName = bookName;
    }
    public String getAuthor() {
        return author;
    }
    public String getBookName() {
        return bookName;
    }
}
//图书管理员
class Bookmaneger implements  Runnable{
    Book book;
    Bookmaneger(Book book){
        this.book = book;
    }
    @Override
    public void run() {
        synchronized (this){
            for (int i =0;i<1000;i++){
                try {
//                    假设网络延迟
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
//                奇偶数时候设置不同图书
                if (i%2==0){
                    book.setAuthor("西游记");
                    book.setBookName("吴承恩");
                }
                else {
                    book.setAuthor("天龙八部");
                    book.setBookName("金庸");
                }
            }
        }
    }
}
//读者线程
class Reader implements Runnable{
    Book book;
    Reader(Book book){
        this.book = book;
    }
    @Override
    public void run() {
        while (true){
            synchronized (this){
//                读取图书信息
                try {
//                    假设网络延迟
                    Thread.sleep(1000);
                    System.out.println(book.getBookName()+"--->"+book.getAuthor());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Library {
    public static void main(String[] args) {
//        公用同一图书对象book
        Book book = new Book();
        Bookmaneger manegerTask = new Bookmaneger(book);
        Reader readerTask = new Reader(book);
//        创建并运行读者和管理员的线程
        new Thread(manegerTask).start();
        new Thread(readerTask).start();
    }
}

 在网络延迟的情况下可能会出现问题:

1.数据不匹配(解决需要-》结果能够一一对应)

2.重复取同一个数据(解决需要--》每次都只取一次,只有更改后再取)

解决方法:

1.数据错乱,根本原因在于多线程下,图书管理员线程在设置图书信息到一半的时候,读者就读取图书信息造成图书信息错乱,解决方法很简单,只需要在book下将所有get和set方法设置为同步代码方法就可以解决数据错乱了

2.其原理也很简单就是因为同步代码块,所以在完成Manger在执行完set前不会执行get。Reader在执行完get前也不会执行set

package Example;
class Book{
//    封装属性
    private String author;
    private String bookName;
//    简化设置和获取属性
    public synchronized void  set(String author,String bookName) {
        this.bookName = bookName;
        this.author = author;
    }
    public String get() {
        return author +"----->"+ bookName;
    }
}
//图书管理员
class Bookmaneger implements  Runnable{
    Book book;
    Bookmaneger(Book book){
        this.book = book;
    }
    @Override
    public void run() {
        synchronized (this){
            for (int i =0;i<1000;i++){
                try {
//                    假设网络延迟
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
//                奇偶数时候设置不同图书
                if (i%2==0){
                    book.set("西游记","吴承恩");
                }
                else {
                    book.set("天龙八部","金庸");
                }
            }
        }
    }
}
//读者线程
class Reader implements Runnable{
    Book book;
    Reader(Book book){
        this.book = book;
    }
    @Override
    public void run() {
        while (true){
            synchronized (this){
//                读取图书信息
                try {
//                    假设网络延迟
                    Thread.sleep(1000);
                    System.out.println(book.get());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Library {
    public static void main(String[] args) {
//        公用同一图书对象book
        Book book = new Book();
        Bookmaneger manegerTask = new Bookmaneger(book);
        Reader readerTask = new Reader(book);
//        创建并运行读者和管理员的线程
        new Thread(manegerTask).start();
        new Thread(readerTask).start();
    }
}

 

虽然解决了数据错乱的问题但是这样的数据出现有重复,我们的目标是需要西游记后输出天龙八部的交替输出。这就是最基础的生产者消费者模型了。

那么如何具体实现呢?就需要用到前面给的方法,等待唤醒机制,即wait();与notify();

其工作原理是设置一个标志位(资源量)当一个Reader读取时候就设置为true,maneger就是false并且进入沉睡。当Reader执行完任务后又将标志位设置为false 并让执行notify()唤醒其他熟睡的线程

案例实现代码:

class  Book{
    private String author;
    private String bookName;
    boolean flag = false;
    int i = 0;

    public synchronized void BookManger(){
//        判断标志位
        if (flag==true){
//            此处如果存在书籍则管理员休息
            try {
                super.wait();
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
//            如果不存在工作管理员就需要进行给书加作者和名字
        }else {
            if (i%2==0){
                this.author = "吴承恩";
                this.bookName = "西游记";
            }else {
                this.author = "金庸";
                this.bookName = "天龙八部";
            }
//            执行完后设置标志位表示已经放好一本新的书了,并且唤醒其他线程
            i++;
            flag = true;
            super.notify();
        }
    }
    public synchronized void  Reader(){
//        判断如果没有书籍则休息
        if (flag==false){
            try {
                super.wait();
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
//            如果有书籍就输出书籍的信息
        }else {
            System.out.println(this.bookName+"----->"+this.author);
            flag = false;
            super.notify();
        }
    }
}
//读者线程
class Reader implements Runnable{
//    设置共同对象用以通信
    Book book;
    Reader(Book book){
        this.book = book;
    }
    @Override
    public void run() {
        while (true){
            book.Reader();
        }
    }
}
//管理员线程
class BookManger implements Runnable{
//    设置同一对象用以通信
    Book book;
    BookManger(Book book){
        this.book = book;
    }
//  线程调用对应方法
    @Override
    public void run() {

        while (true){
            book.BookManger();
        }
    }
}
public class ExampleThreadtest {
    public static void main(String[] args) {
//        创建共有对象book
        Book book = new Book();
        BookManger mangerTask = new BookManger(book);
        Reader readerTask = new Reader(book);
//        线程启动
        new Thread(mangerTask).start();
        new Thread(readerTask).start();
    }
}

面试题: Java 如何实现多线程之间的通讯和协作?

1.可以通过中断 和 共享变量的方式实现线程间的通讯和协作

比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

Java中线程通信协作的最常见的两种方式:

1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

线程间直接的数据交换:

通过管道进行线程间通信:1)字节流;2)字符流


 3.模拟生产者与消费者

模拟生产者与消费者

通过上面一个案例应该就有对生产者消费者模型有初步了解,下面将举一个十分经典的消费者生产者模型让大家有更深层次的理解

案例目标:设计一个生产计算机类与一个搬运计算机类,要求是生产者生产一台计算机就要搬走一台计算机,如果没有新的计算机那么搬运工就要等待新的计算机产出,如果生产出的计算机没有被搬走就要等待搬运者将计算机搬走,最后搬运统计搬运走的电脑个数

案例代码:

package Test1402;
class Computer{
//    设置标志位
    private int flag = 0;
    private  int id=1;
    private int count = 0;
    
    //模拟生产函数
    public void produce(){
        synchronized (this){
            while (true){
                if (flag == 1){
                    try {
                        super.wait();
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                System.out.println("生产电脑第"+id++);
                count++;
                flag = 1;
                super.notify();
            }
        }
    }
//    模拟搬运函数
    public  void  Carry(){
        synchronized (this){
            while (true){
                if (flag ==0){
                    try {
                        super.wait();
                        Thread.sleep(1000);
 
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                System.out.println("搬运电脑"+(id+-1));
                flag = 0;
                super.notify();
            }
        }
    }
}
 
class Producer implements Runnable{
    Computer computer;
 
    Producer(Computer computer){
        this.computer = computer;
    }
    @Override
    public void run() {
 
        computer.produce();
    }
}
 
class Carryer implements  Runnable{
    Computer computer;
    Carryer(Computer computer){
        this.computer =computer;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        computer.Carry();
    }
}
 
public class javaDemo {
    public static void main(String[] args) {
        Computer computer = new Computer();
        new Thread(new Producer(computer)).start();
        new Thread(new Carryer(computer)).start();
    }
}


4.守护线程

一个进程的运行往往可能需要十分多的子进程辅助运行,比如聊天软件,主线程是软件的使用。而所有的聊天对象都是子线程可以分别接收消息。当软件关闭主线程时候即关闭软件使用时候,此时子线程的存在就没有了意义。就会自动关闭。这样的子线程就叫做守护线程

设置守护线程

Thread 对象.setDeamon(true/false);true-》开启守护线程,false-关闭守护线程

具体应用案例:

package Example1413;
public class javaDemo {
    public static void main(String[] args) {
//        主线程
        new Thread( ()->{
//            执行输出三次
        for (int i=0;i<3;i++){
            try {
                Thread.sleep(300);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+i);
        }
        },"userThread").start();

//        守护线程
        Thread daemon = new Thread(()->{
//            正常情况下应该执行输出10000次
            for (int i=0;i<10000;i++){
                try {
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+i);
            }
        },"daemonThread");
//        设置守护模式
        daemon.setDaemon(true);
        daemon.start();
    }

}

 


5.volatile关键字

一般情况下比如之前的售票员售票其调用ticket时候是进行先复制其数据副本再通过加载-》使用-》赋值-》存储-》写入才对内存的数据ticket进行同步,就是线程操作的数据都只是原始数据的备份,在操作完成后再和原始数据进行替换。而volatile则不需要这些数据备份直接操作内存的原始数据

好处:volatile关键字可以直接对内存进行操作,就不需要同步数据了,所以可以减少程序运行的时间

使用案例代码:

package Example1414;

class sale implements Runnable{
    private volatile int ticket =100;

    @Override
    public void run() {
        synchronized (this){
            while (ticket>0){
                try {
                    Thread.sleep(100);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"售卖出"+ticket--+"张票");
            }
        }
    }
}

public class javaDemo {
    public static void main(String[] args) {
        sale s = new sale();
        new Thread(s,"售票员A").start();
        new Thread(s,"售票员B").start();
        new Thread(s,"售票员C").start();
    }
}

*面试题:volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

 *面试题:既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?

volatile修饰的变量在各个线程的工作内存中不存在一致性的问题(在各个线程工作的内存中,volatile修饰的变量也会存在不一致的情况,但是由于每次使用之前都会先刷新主存中的数据到工作内存,执行引擎看不到不一致的情况,因此可以认为不存在不一致的问题),但是java的运算并非原子性的操作,导致volatile在并发下并非是线程安全的。

  *面试题:请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。

在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。

主内存和工作内存之间的交互分为8个原子操作:

    lock
    unlock
    read
    load
    assign
    use
    store
    write

volatile修饰的变量,只有对volatile进行assign操作,才可以load,只有load才可以use,,这样就保证了在工作内存操作volatile变量,都会同步到主内存中。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alphamilk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值