Java面试 多线程篇(线程安全、同步锁和单例化)

线程安全问题

线程的安全问题主要体现在,当需要访问公共资源是两个多个线程可能会出现问题
举个例子

class YdThread implements Runnable {

    private int num = 10;

    public void run() {
        while(num > 0) {
            try {
                Thread.sleep(100);//加个sleep便于观察
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.print(num+" ");
            num--;
        }
    }
}
/**
*主函数中调用
*/
public class Main {

    /**
     * 主函数
     */
    public static void main(String[] args) {
        YdThread td = new YdThread();
        Thread  t1 = new Thread(td);
        Thread  t2 = new Thread(td);
        t1.start();
        t2.start();
    }
}

最终打印的结果:
10 9 8 8 6 6 4 4 2 2 0
不仅仅出现了0 其中2 4 6 8 都出现的多次

当num=1时
因为在线程1进入while(num > 0)之后睡眠,然后线程2 进入此时num 还是大于0的
因此两个线程都处于while循环中
接下来一个线程执行下面代码另一个线程也执行因此会出现打印0和重复的情况

线程安全问题出现的条件:

  • 多个线程操作共享数据
  • 操作共享数据的语句有多条

这就是线程的安全问题,因此就出现的关键字synchronized,相当于加上了一把锁 ,产生两种同步线程的方法:


同步函数和同步代码块synchronized

1、同步函数

public synchronized void sale(){
}
在方法前加synchronized关键字,为sale方法加上一把锁,当一个线程进入执行的时候获取的该对象锁,其他线程就不能进入,只有当该线程执行完之后释放对象锁,其他线程才能进入
同步函数的对象锁是this是固定的,也就是当前的对象

2、同步代码块:

synchronized(对象锁){
. 代码
}

同步代码块是采取ssynchronized修饰代码块来进行同步
需要再synchronized()中加入任意对象作为对象锁,该锁可以是任意的对象

同步代码块和同步函数的区别:

同步代码块的锁是任意的,同步函数的锁固定是this
建议使用同步代码块

同步能解决线程安全问题,但是会降低效率,因为同步外的线程都需要判断同步锁


关键字volatile

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

并发编程会遇到三个问题。原子性问题,可见性问题,有序性问题

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行


线程的死锁问题

如果线程进行了同步的嵌套,就会产生死锁问题:
示例:

面试时可能会需要写一个死锁

public class LockDemo {

    /**
    *主函数
    */
    public static void main(String args[]){
        Demo d = new Demo();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();
        d.flag = false;//将flag赋值为false让线程二执行else中的代码
        t2.start();
    }


}
class Demo implements Runnable{
     static boolean flag = true;
    Object locka = new Object();
    Object lockb = new Object();

    public void run(){

        if(flag){
            while(true){
                synchronized(locka){
                    System.out.println(Thread.currentThread().getName()+"----get----locka");
                    try{
                         Thread.sleep(100);
                     }catch(InterruptedException e){}//让线程1睡眠一段时间加大产生思索概率便于观察
                synchronized(lockb){
                    System.out.println(Thread.currentThread().getName()+"----get----lockb");
                }
            }
            }
        }else{
            while(true){
                synchronized(lockb){
                System.out.println(Thread.currentThread().getName()+"----get----lockb");
                synchronized(locka){
                    System.out.println(Thread.currentThread().getName()+"----get----locka");
                }
            }
            }
        }
    }
}

加入循环也是为了便于观察,当1线程获得对象锁locka,等待对象锁lockb;
2线程获得对象锁lockb,等待对象锁locka,因此处于死锁状态


Java单例化的两种方式

java编程中可能会遇到需要单例化的情况,比如多个客户端但是只能有一个服务端
因此服务端对象需要进行单例化

两种方式都需要把构造方法变成私有,不能通过构造方法获取对象

1、饿汉式

饿汉式

public class SingleThread {
    /**
     * 饿汉式
     */
    private static final SingleThread s = new SingleThread();//该对象为final只能赋值一次
    private SingleThread() {
    }
    public static SingleThread getInstance() {
        return s;
    }
}

懒汉式:

public class SingleThread {
    /**
     * 懒汉式
     */
    private static SingleThread s = null;
    private SingleThread() {
    }
    public static SingleThread getInstance() {
        if(s == null){
            s = new SingleThread();//创建对象
        }
        return s;//返回对象
    }
}

懒汉式会出现安全问题,因为对共享数据s有多条操作语句,因此需要用同步解决,
利用同步函数

public class SingleThread {
    /**
     * 懒汉式
     */
    private static SingleThread s = null;
    private SingleThread() {
    }
    public synchronized static SingleThread getInstance() {//加上了同步锁
        if(s == null){
            s = new SingleThread();//创建对象
        }
        return s;//返回对象
    }
}

此方法虽然解决了安全问题但是以后每次都需要判断同步锁,降低了效率,因此改用同步代码块

public class SingleThread {
    /**
     * 懒汉式
     */
    private static SingleThread s = null;
    private  SingleThread() {
    }
    public  static SingleThread getInstance() {
        if(s == null){
            synchronized(SingleThread.class){
                if(s == null){
                    s = new SingleThread();//创建对象
                }
            }
        }
        return s;//返回对象
    }
}

此处因为该方法是静态的,因此在类创建时就已经存在,此时还没有产生该类的实例,所以不能用this作为对象锁,使用 类名+class 获取的是该类字节码所属对象(java被转化为字节码文件时会分配当前的class文件所处的对象)

以上代码看似没问题,其实不然
在 Java 中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的。对于除了 long 和 double 的基本类型,双重检查模式是适用 的。。JDK5.0以后版本若s为volatile则可行

public class SingleThread {
    /**
     * 懒汉式
     */
    private volatile static SingleThread s = null;
    private SingleThread() {
    }
    public  static SingleThread getInstance() {
        if(s == null){
            synchronized(SingleThread.class){
                if(s == null){
                    s = new SingleThread();//创建对象
                }
            }
        }
        return s;//返回对象
    }
}

饿汉式比较简单,也不会出现安全问题,因此推荐使用饿汉式,但是面试时一般都考懒汉式

你可能感兴趣的