Java多线程笔记

5 篇文章 0 订阅

学习多线程与并发,要着重“外炼互斥,内修可见,内功有序”。

一、Java多线程技能

1.1、线程的实现与执行

创建线程有两种方法:

  1. 继承Thread类,并重写run()方法,在run()方法中添加线程要执行的任务代码
  2. 实现Runnable接口,并重写run()方法,在run()方法中添加线程要执行的任务代码

启动线程需要调用Thread类的start方法:

MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();

线程启动后会自动调用线程对象中的run()方法,run()方法里面的代码就是线程对象要执行的任务。

start()方法比较耗时,原因是执行了多个步骤:

  1. 通过JVM告诉操作系统创建Thread;
  2. 操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象;
  3. 操作系统对Thread对象进行调度,以确定执行时机;
  4. Thread在操作系统中被成功执行。

如果调用代码"thread.run()",而不是"thread.start()",那么就不是异步执行了。

1.2、synchronized加锁

synchronized可以对任意对象及方法加锁,加锁的这段代码称为互斥区,多个线程在执行互斥区的代码时,以排队的方式进行处理。

在Web开发中,Servlet对象本身就是单例的,所以为了不出现非线程安全问题,建议不要在Servlet中出现实例变量。

1.3、线程方法

1、currentThread()方法可返回代码段正在被哪个线程调用:

2、isAlive()方法的功能是判断指定的线程是否存活(线程已经启动,且尚未终止的状态即为存活状态):

3、sleep(long millis)方法的作用是在指定的时间内让当前线程休眠(暂停执行),不会释放锁,休眠之后的线程不会继续占用CPU资源:

4、sleep(long millis, int nanos)在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠,并不会释放锁:

5、StackTraceElement[] getStackTrace()方法用于返回该线程堆栈跟踪元素数组,如果该方法尚未启动或已经终止,返回一个零长度数组,如果返回的不是零长度的,则第一个元素代表堆栈顶,是该线程中最新的方法调用:

6、static void dumpStack()方法的作用是将当前线程的堆栈跟踪信息输出至标准错误流,该方法仅用于调试:

7、static Map<Thread, StackTraceElement[]> getAllStackTraces() 方法的作用是返回所有活动线程的堆栈跟踪的一个映射:

8、getId()方法用于取得指定线程的唯一标识:

9、join()方法用于等待指定线程执行完毕,内部使用wait()方法进行等待,会释放锁:

10、join(long)方法中的参数用于设定等待的时间,时间到了之后,不管指定线程是否执行完毕,都会继续执行,join(long)执行之后,会释放锁:

11、join(long millis, int nanos)方法的作用是等待毫秒+纳秒的时间。

MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//1、currentThread()
//下面代码输出的是"Thread-0"
System.out.println(thread.getName());
//下面代码输出的是"main"
System.out.println(thread.currentThread().getName());
//下面代码输出的是"main"
System.out.println(Thread.currentThread().getName());
//2、isAlive()
//下面代码输出的是ture
System.out.println(thread.isAlive());
//3、sleep(long millis)
//main线程休眠1s
thread.sleep(1000);
//4、sleep(long millis, int nanos)
//main线程休眠1s
Thread.sleep(1000);
//5、StackTraceElement[] getStackTrace()
//获取该线程堆栈跟踪元素数组
StackTraceElement[] sList = Thread.currentThread().getStackTrace();
for(int i = 0; i < sList.length; i++) {
    //输出的是目前所在的方法名称
    System.out.println(sList[i].getMethodName());
}
//6、static void dumpStack()
//将当前线程的堆栈跟踪信息输出至标准错误流
Thread.dumpStack();
//7、static Map<Thread, StackTraceElement[]> getAllStackTraces()
//返回所有活动线程的堆栈跟踪的一个映射
Map<Thread, StackTraceElement[]> map = Thread.currentThread().getAllStackTraces();
//8、getId()
//输出的是"12   Thread-0"
System.out.println(thread.getId() + "   " + thread.getName());
//输出的是"12   Thread-0"
System.out.println(Thread.currentThread().getId() + "   " + Thread.currentThread().getName());
//9、join()
thread.join();

停止线程:

  1. stop(),强行终止线程,容易造成业务处理的不确定性,已经不建议使用。
  2. interrupt(),在指定线程中做一个停止标记,不能真正停止线程。

判断线程是否为中断状态:

  1. Thread.interrupted():测试当前线程是否已经是中断状态(执行interrupt()后会变为该状态),执行后具有改变中断状态标志值为false。
  2. thread.isInterrupted():测试目标线程是否已经是中断状态(执行interrupt()后会变为该状态),不改变状态。

interrupt()和sleep()方法碰到一起就会出现异常,不管其调用顺序如何。

虽然使用 “return;” 较 “抛异常” 法在代码结构上可以更加方便地实现线程的停止,不过还是建议使用 “抛异常” 法,因为在catch块中可以对异常的信息进行统一的处理。

暂停线程:

  1. suspend() 暂停线程,并不会释放锁
  2. resume() 恢复线程的执行

这两个方法已不建议使用,因为如果这两个方法使用不当,极易造成公共同步对象被独占,其他线程无法访问公共同步对象的结果(死锁),另外也容易导致数据不完整的情况。

想要对线程进行暂停与恢复的处理,可使用wait()、notify()或notifyAll()方法。

放弃CPU资源:

Thread.yield();

该方法的作用是放弃当前的CPU资源,让其他任务去占用CPU执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

线程优先级:

//设置指定线程的优先级
thread.setPriority(10);
//获取指定线程的优先级
System.out.println(thread.getPriority());

在java中,线程的优先级分为1-10共10个等级。

线程的优先级具有继承性,如果A线程启动B线程,则A、B两个线程的优先级是一样的。

守护线程:

//将目标线程设置为守护线程
thread.setDaemon(true);

守护线程是一种特殊的线程,当进程中不存在非守护线程以后,守护线程就会自动销毁。

二、对象及变量的并发访问

2.1、基本概念

方法中的变量不存在非线程安全问题,永远都是线程安全的,这是因为方法内部的变量具有私有特性。

两个线程同时访问同一个对象中的同步方法时一定是线程安全的。

synchronized在字节码指令中的原理:使用了flag标记ACC_SYN-CHRONIZED。

在方法声明处添加synchronized并不是锁方法,而是锁当前类的对象,在Java中只有“将对象作为锁”这种说法,并没有”锁方法“这种说法。

锁重入:

synchronized具有重入锁的功能,“可重入锁”是指自己可以再次获取自己的内部锁(在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的);

当存在父子类继承关系时,子类是完全可以通过锁重入调用父类的同步方法的(其实获取的还是当前对象的锁)。

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

重写方法如果不使用synchronized关键字,即是非同步方法,使用后变成同步方法。

2.2、synchronized同步语句块

synchronized方法是将当前对象作为锁,synchronized代码块可以将任意对象作为锁。

public void pp() {
        synchronized (this) {
        }
}
//或者
public void pp(User user) {
        synchronized (user) {
        }
}

不同对象的方法,如果以同一个对象作为锁,则它们也是同步的,所以一般不使用String对象作为锁。

在静态static方法上使用synchronized关键字声明同步方法时,使用当前静态方法所在类对应Class类的单例对象作为锁。

//使用Class锁
class Solution {
	synchronized public static void pp() {
        synchronized (this) {
        }
	}
}
//或者
class Solution {
	public void pp(User user) {
        synchronized (Solution.class) {
        }
	}
}

Class锁可以对类的所有对象实例起作用。

死锁是程序设计的bug,在设计程序时要避免双方互相持有对方的锁,只要互相等待对方释放锁,就有可能出现死锁。

通常情况下,一旦持有锁后就不再对锁对象进行更改,因为一旦更改就有可能出现一些错误。

2.3、volatile关键字

**可见性:**volatile修饰类中的变量,可以使该变量对所有线程可见,如果不用volatile进行修饰的话,在线程启动后,类中的变量会被拷贝到线程的私有堆栈中,该线程默认访问的是私有堆栈中的数据,别的线程修改该变量的值的话,改的是公有堆栈中的变量值,如果使用volatile修饰变量,则各个线程访问该变量时,强制从公共堆栈中进行取值。

**原子性:**volatile并不能解决原子性问题,原子性还需用synchronized来解决,如果是解决i++操作原子性的话,可以使用Atomic原子类。

AtomicInteger atomicInteger = new AtomicInteger(0);
//atomicInteger.incrementAndGet()即为+1操作,实现了原子性
System.out.println(atomicInteger.incrementAndGet());

**禁止代码重排序:**volatile和synchronized都可以禁止代码重排序。

在Java程序运行时,JIT(即时编译器)可以动态地改变程序代码运行的顺序,这样可以提高程序的运行效率,这就是代码重排序。

三、线程间通信

3.1、基本原理

拥有相同锁的线程才可以实现wait/notify机制,wait()方法使线程暂停运行并立即释放锁,notify()方法通知暂停的线程继续运行,但要等同步代码块执行完毕才会释放锁。

Java为每个对象都实现了wait()和notify()方法,它们必须用在被synchronized同步的Object的临界区内。

如果发出notify操作时没有处于wait状态中的线程,那么该命令会被忽略。

notify()方法按照执行wait()方法的顺序唤醒等待同一锁的“一个”线程,使其进入可运行状态。

notifyAll()方法执行后,会按照执行wait()方法相反的顺序依次唤醒全部的线程。

当线程调用wait()方法后,再对该线程对象执行interrupt()方法会出现InterruptedException异常。

wait(Long)方法的功能是等待某一时间内是否有线程对锁进行notify()通知唤醒,如果超过这个时间则线程自动唤醒,能继续向下运行的前提是再次持有锁。

3.2、生产者/消费者模式

在多生产者和多消费者模式中,容易出现“假死”(大量线程进入waiting状态),其原因是notify()方法有可能连续唤醒同类,解决方法是改用notifyAll()方法。

想要实现任意数量的几对几生产与消费的示例,可使用while结合notifyAll()的方法,这种组合具有通用性。

class Kk {
    public static List<String> stringList = new ArrayList<>();
    //生产者
    public void add(Kk kk) throws InterruptedException {
        synchronized (kk) {
            while (stringList.size() >= 10) {
                kk.wait();
            }
            Thread.sleep(1000);
            stringList.add(LocalDateTime.now().toString());
            System.out.println("存入" + LocalDateTime.now());
            kk.notifyAll();
        }
    }

    //消费者
    public void pop(Kk kk) throws InterruptedException {
        synchronized (kk) {
            while (stringList.size() <= 0) {
                kk.wait();
            }
            Thread.sleep(1000);
            System.out.println("取出" + stringList.get(stringList.size() - 1));
            stringList.remove(stringList.size() - 1);
            kk.notifyAll();
        }
    }
}

3.3、通过管道流进行线程间通信

PipedInputStream(管道输入流)与PipedOutputStream(管道输出流):

class MyThread implements Runnable {


    PipedInputStream pipedInputStream;

    MyThread(PipedInputStream pipedInputStream) {
        this.pipedInputStream = pipedInputStream;
    }

    @Override
    public void run() {
        try {
            while (true) {
                //从管道输入流中读数据
                System.out.println("读到" + pipedInputStream.read());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyThread1 implements Runnable {

    PipedOutputStream pipedOutputStream;

    MyThread1(PipedOutputStream pipedOutputStream) throws IOException {
        this.pipedOutputStream = pipedOutputStream;
    }

    @Override
    public void run() {
        try {
            while (true) {
                String a = LocalDateTime.now().toString();
                System.out.println("存入" + a);
                //向管道输出流中写数据
                pipedOutputStream.write(a.getBytes());
                Thread.sleep(1000);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Solution {

    public static void main(String[] args) throws InterruptedException, IOException {

        PipedOutputStream pipedOutputStream = new PipedOutputStream();
        PipedInputStream pipedInputStream = new PipedInputStream();
        //绑定输入流与输出流
        pipedInputStream.connect(pipedOutputStream);

        MyThread myThread = new MyThread(pipedInputStream);
        Thread thread = new Thread(myThread);
        thread.start();

        MyThread1 myThread1 = new MyThread1(pipedOutputStream);
        Thread thread1 = new Thread(myThread1);
        thread1.start();

    }

}

PipedWriter与PipedReader的使用与上面两个类相同。

3.4、ThreadLocal

ThreadLocal用于使每一个线程拥有自己的变量,ThreadLocal的主要作用是将数据放入当前线程对象中的Map中,这个Map是Thread类的实例变量。每个线程中的Map存有自己的数据,Map中的key存储的是ThreadLocal对象,value就是存储的值。每个Thread中的Map值只对当前线程可见,其它线程不可以访问。

ThreadLocal向当前线程对象中存值就使用set(),取值就使用get(),默认值为null:

class Solution {
    public static ThreadLocal t1 = new ThreadLocal();

    public static void main(String[] args) {
        t1.get();
        t1.set("111");
    }
}

3.5、InheritableThreadLocal

使用类InheritableThreadLocal可使子线程继承父线程的值。

四、Lock对象

4.1、ReentrantLock类

4.1.1、基本使用

ReentrantLock 的构造方法 :ReentrantLock(boolean fair),当fair为true,则为公平锁,若fair为false,则为非公平锁,默认为false。

ReentrantLock使用 lock.lock() 来加锁,使用 lock.unlock() 来释放锁,这两个方法成对使用:

class MyThread implements Runnable{
    private Lock lock = new ReentrantLock();

    public void run() {
        lock.lock();

        lock.unlock();
    }
}

Lock对象借助Condition对象可以实现wait/notify模式,在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择地进行线程通知,在调度线程上更加灵活。

Condition对象的作用是控制并处理线程的状态,它可以用await()方法使线程释放锁,使线程处于wait状态,也可以用signal()或者signalAll()方法通知暂停的线程继续运行。

Lock + Condition实现生产者/消费者模式:

class MyPri{
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();

    private volatile int a = 0;

    public void pp(){
        try {
            lock.lock();
            if(a == 1) {
                conditionA.await();
                return;
            }
            System.out.println("pp");
            a = 1;
            Thread.sleep(1000);
            conditionA.signal();
        }catch (Exception e) {
            System.out.println(e);
        }finally {
            lock.unlock();
        }
    }

    public void kk(){
        try {
            lock.lock();
            if (a == 0) {
                conditionA.await();
                return;
            }
            System.out.println("kk");
            a = 0;
            conditionA.signal();
        }catch (Exception e) {
            System.out.println(e);
        }finally {
            lock.unlock();
        }
    }
}

可以采用signalAll()解决假死的问题。

4.1.2、常用方法

1、public int getHoldCount()方法的作用是查询当前线程保持此锁定的个数,执行lock()方法进行锁重入导致count计数+1,执行unlock()方法会使count()计数-1;

2、public final int getQueueLength()方法的作用是返回正等待获取此锁的线程估计数;

3、public int getWaitQueueLength(Condition condition)方法的作用是返回等待与此锁相关的给定条件Condition的线程估计数;

4、public final boolean hasQueuedThread(Thread thread)方法的作用是查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中;

5、public final boolean hasQueuedThreads()方法的作用是查询是否有线程正在等待获取此锁;

6、public boolean hasWaiters(Condition condition)方法的作用是查询是否有线程正在等待与此锁有关的condition条件,也就是是否有线程执行了condition对象中的await()方法而呈等待状态;

7、public final boolean isFair()方法的作用是判断是不是公平锁;

8、public boolean isHeldByCurrentThread()方法的作用是查询当前线程是否保持此锁;

9、public boolean isLocked()方法的作用是查询此锁是否由任意线程保持,并没有释放;

10、public void lockInterruptibly()方法的作用是当某个线程尝试获得锁并且阻塞在lockInterruptibly()方法时,该线程可以被中断,该方法可以替代.lock()方法使用;

11、public boolean tryLock()方法的作用是嗅探拿锁,如果当前线程发现锁被其它线程持有,则返回false,程序继续后面的代码,而不是呈阻塞等待锁的状态,该方法获取锁的方法是非公平方法;

12、public boolean tryLock(long timeout, TimeUnit unit)方法的作用是嗅探拿锁,如果当前线程在指定的时间timeout内持有了锁,则返回值是true,超过时间返回false;

13、public boolean await(long time, TimeUnit unit)方法是在await()方法的基础上,拥有了自动唤醒线程的功能;

14、public long awaitNanos(long nanosTimeout)方法是在await()方法的基础上,拥有了以纳秒为单位唤醒线程的功能;

15、public boolean awaitUntil(Date deadline)方法是在await()方法的基础上,拥有了在指定的Date结束等待的功能;

16、public void awaitUninterruptibly()方法的作用是在await()方法的基础上,使线程在等待的过程中,不允许被中断;

lock.getHoldCount();
lock.getQueueLength();
lock.getWaitQueueLength(conditionA);
myPri.lock.hasQueuedThread(thread);
myPri.lock.hasQueuedThreads();
lock.hasWaiters(conditionA);
lock.isFair();
lock.isHeldByCurrentThread();
lock.isLocked();
lock.lockInterruptibly();
lock.tryLock();
lock.tryLock(3, TimeUnit.SECONDS);
conditionA.await(3, TimeUnit.SECONDS);
conditionA.await(10000L);
conditionA.await(new Date());
conditionA.awaitUninterruptibly();

4.2、ReentrantReadWriteLock类

ReentrantLock类具有完全互斥排他的效果,这样效率是非常低下的,ReentrantReadWriteLock类则不同,读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥;

class MyThread implements Runnable{
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void run() {
        lock.readLock().lock();
        lock.readLock().unlock();
        lock.writeLock().lock();
        lock.writeLock().unlock();
    }
}

五、定时器Timer

六、单例模式实现线程安全

单例模式中分为饿汉模式与懒汉模式,饿汉模式是指使用类的时候,对象已经创建完毕,懒汉模式是指调用get()方法时,实例才被工厂创建。

6.1、DCL机制(双检查锁)

单例模式实现线程安全可以使用DCL机制(双检查锁):

class MyThread{
    private volatile static MyThread myThread;

    private MyThread() {}

    public static MyThread getMyThread() {
        try {
            if(null == myThread) {
                synchronized (MyThread.class) {
                    myThread = new MyThread();
                }
            }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return MyThread.myThread;
    }
}

使用volatile修改变量myObject使该变量在多个线程间可见,另外也禁止了myObject = new MyObject()代码重排序。

禁止对象new的重排序是因为对象的new在内部分为三步:

  1. 分配对象的内存空间
  2. 初始化对象(调用构造方法)
  3. 设置实例地址指向刚分配的内存地址

这三步如果改变顺序,就可能出现:这个线程先执行1、3步骤,然后另一个线程检查myObject != null,然后去访问没有初始化过的对象,拿到了错误的值。

return MyThread.myThread 可以解决序列化与反序列化时多线程创建新对象的问题,这样的写法会复用原对象,而直接return myThread可能会创建一个新对象。

6.2、静态内置类

单例模式实现线程安全也可以使用静态内置类:

class MyThread{

    private static class MyThreadHandler {
        private static MyThread myThread = new MyThread();
    }

    private MyThread() {}

    public static MyThread getMyThread() {
        return MyThreadHandler.myThread;
    }
}

6.3、static 代码块

单例模式实现线程安全也可以使用 static 代码块:

class MyThread{
    private volatile static MyThread myThread;

    private MyThread() {}
    
    static {
        myThread = new MyThread();
    }

    public static MyThread getMyThread() {
        return MyThread.myThread;
    }
}

6.4、enum 枚举数据类型

单例模式实现线程安全也可以使用 enum 枚举数据类型:

enum MyThread{
    myThreadFactory;
    
    private MyThread myThread;

    private MyThread() {}

    public static MyThread getMyThread() {
        return MyThread.myThreadFactory.myThread;
    }
}

enum枚举数据类型的特性和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,可以应用这个特性实现单例模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值