Java多线程学习笔记(三)

同步方法解决实现Runnable的线程安全问题

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。

package com.xzc;

class Window3 implements Runnable {
    public Window3() {
    }

    private int ticket;

    public Window3(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
        }
    }
}

public class Test3 {
    public static void main(String[] args) {
        Window3 w = new Window3(100);
        new Thread(w, "窗口一").start();
        new Thread(w, "窗口二").start();
        new Thread(w, "窗口三").start();
    }

}

输出结果:
在这里插入图片描述
注意这里:private synchronized void show()就是同步方法处理。
这里的锁没有显示出来,但是也是有锁,在show方法里,锁就是this

同步方法解决继承Thread的线程安全问题

此时不能像之前一样,直接把show方法定义为synchronized的,因为锁的不唯一。如果非想用同步方法解决,就要在synchronized前面加一个static声明为静态方法。

package com.xzc;

class Window4 extends Thread {
    public Window4() {

    }

    public Window4(String name) {
        super(name);
    }

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket--);
        }
    }
}

public class Test4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4("窗口一");
        Window4 w2 = new Window4("窗口二");
        Window4 w3 = new Window4("窗口三");
        w1.start();
        w2.start();
        w3.start();
    }

}

输出结果:
在这里插入图片描述
此时,我们的锁是当前类Window4.class

同步方法总结

同步方法仍然涉及到锁,只是不需要我们显示声明

非静态的同步方法,同步监视器是this

静态的同步方法,同步监视器是当前类本身(即Class类对象)

线程安全的单例模式之懒汉式

package demo01;

public class BankTest {

}

//懒汉式
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static  Bank getInstance(){
        if (instance == null){
           instance = new Bank();   
        }
        return instance;
    }
}

这样是不线程安全的,如果我们有多个线程run方法调用getInstance方法,其中一个线程进来,首次instance肯定是null,然后刚进if语句,可能会被阻塞,即使不被阻塞,cpu也可能切换到线程2,另外一个线程进来,那么instance会先后两次赋值,显然是不对的,因为instance是静态Bank类型变量。instance相当于是共享数据了。下面用同步方式修改为线程安全的:

package demo01;

public class BankTest {

}

//懒汉式
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static  Bank getInstance(){
        //方式一:效率稍差
        /*synchronized(Bank.class) {
            if (instance == null) {
                instance = new Bank();
            }
            return instance;
        }*/
        //方式二
        if (instance == null){
            synchronized (Bank.class){
                if(instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

我们先看方式一,return如果在synchronized同步代码块里面,为什么效率会低,这是因为不管instance是不是null,都进入同步代码块,如果线程非常多,就会产生效率低下问题,假设线程一拿到锁,产生一个instance返回,后面的所有线程还要在同步代码块前面等着,事实上这是无意义的,因为我们已经有instance了,后面的线程拿着instance返回就好了。而方式二就修正了这个问题,我们在同步代码块面前先判断instance是不是空,并且把return instance提出来,这样线程一二三可能在同步代码块中抢一下锁,卡一会,但是后面的所有线程进来,直接就判断instance不是null了,然后直接返回instance就好了。
这样我们就把单例模式的懒汉式修改为线程安全的了。

线程死锁

在这里插入图片描述
当我们有两个锁嵌套的时候,就比较容易出现死锁问题:

package demo01;

public class ThreadTest {
    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");
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(() -> {
            synchronized (s2) {
                s1.append("c");
                s2.append("3");
                
                synchronized (s1) {
                    s1.append("d");
                    s2.append("4");
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }).start();
    }
}

输出结果
在这里插入图片描述
这段代码其实就有死锁隐患,这是线程顺次执行完了,我们用sleep阻塞一下:

package demo01;

public class ThreadTest {
    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 {
                        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(() -> {
            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();
    }
}

当我们在嵌套的分解处加入sleep进行阻塞后,就出现了死锁问题,这段代码执行起来,就和死循环一样,不会终止,但是也不会报错,这是因为我们第一个线程执行,拿到s1这把锁,执行sleep阻塞,这时候第二个线程很大概率也被调度,拿到s2这把锁,然后执行sleep阻塞,这时候第一个线程手里有s1这把锁,醒过来后发现没有s2这把锁,没法进去下面的同步代码段,这是因为s2被第二个线程拿了,同理s2醒过来后也拿不到s1,就僵持住了,发生了死锁。
也就是发生了:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。

Lock

在这里插入图片描述

package demo01;

import java.util.concurrent.locks.ReentrantLock;

//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{

    private int ticket = 100;
    //定义reentrantlock对象
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                //调用lock方法,锁住
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
                }
                else {
                    break;
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        new Thread(w,"Window - 1").start();
        new Thread(w,"Window - 2").start();
        new Thread(w,"Window - 3").start();
    }

}

输出结果:
在这里插入图片描述
可以看到我们使用Reentrantlock类可以实现同步的方式,但是需要我们加锁解锁,synchronized是自动执行,加锁解锁一般就如上使用try-finally执行,一般就是先定义Reentrantlock对象,然后try里面锁住,finally里面解锁。Reentrantlock构造器还可以有一个参数:boolean fair,如果为true,则实现公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

package demo01;

import java.util.concurrent.locks.ReentrantLock;

//Lock锁解决线程安全问题-JDK5.0新增
class Window implements Runnable{

    private int ticket = 100;
    //定义reentrantlock对象
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while (true){
            try {
                //调用lock方法,锁住
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
                }
                else {
                    break;
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        new Thread(w,"Window - 1").start();
        new Thread(w,"Window - 2").start();
        new Thread(w,"Window - 3").start();
    }

}

这里我们用了公平锁,结果:
在这里插入图片描述
我们从这可以看到,如果三个线程一开始的等待时间确定了,后面一定是一样的,1进拿锁,3进,2进则3一定等的比2长,3拿锁,1执行完了后2一定比1等的长,2拿锁,后面同理,所以公平锁可以看做先进先出。

synchronized与Lock的异同

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。

一般优先顺序(实际上都一样):
Lock->同步代码块->同步方法

练习

在这里插入图片描述

package demo01;

import java.util.concurrent.locks.ReentrantLock;

class User implements Runnable {
    private int money;
    public User() {
    }
    public User(int initMoney) {
        money = initMoney;
    }
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                lock.lock();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                money += 1000;
                System.out.println(Thread.currentThread().getName() + "存了1000元," + "现有资金:" + money);
            } finally {
                lock.unlock();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        User u = new User(0);
        new Thread(u, "Human - 1").start();
        new Thread(u, "Human - 2").start();
    }
}

输出结果:
在这里插入图片描述
这里注意我们用的lock方式,因为采用实现runnable接口的方式来多线程,所以我们的锁是同一个,如果我们采用继承thread类方式,我们要把lock设为static的,以保证锁的唯一。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值