线程学习笔记(三)

线程学习笔记(二)

目录

synchronized

volatile 

可见性

原子性

AtomicInteger 

练习

ReentrantLock

练习


synchronized

1.synchronized锁的不是代码块,而是对象,等于3个人上厕所,第一个人获得锁,锁起来,其他人就进不来了,得等解锁

2.使用代码块,synchronized的锁其实是指向堆里面那个,而不是栈那个,不然o变化了,锁就没了,其他线程就可以运行了,所以不要改变锁对象指向的引用。

3.当第一个线程获取锁之后,其他线程等待,等执行完毕,释放锁,第二个线程立即获取锁,只要有一个人获得锁其他人就拿不到锁,所以这叫互斥锁。

4.一个synchronized代码块是个原子操作

5.静态方法里的锁,是class,不能this,因为静态方法,不需要new对象出来,直接类名.方法就行

class window extends Thread {
    private static int n = 0;

    @Override
    public void run() {
        add();
    }
    
    public static void add() {
        synchronized (window.class) {
            n++;
            System.out.println(Thread.currentThread().getName() + "=" + n);
        }
    }
}

6.同步和非同步方法是否可以同时调用?答案是可以

看以下例子,只有需要锁的方法,才不能被同时执行

class T {
    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName()+"m1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" m1 end");
    }

    public void m2(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" m2");
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(()->t.m1(),"t1").start();
        new Thread(()->t.m2(),"t2").start();
    }
}
t1m1 start...
t2 m2
t1 m1 end

7.一个同不方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说synchronized获得的锁是可重入的,这里是继承中可能发生的情形,子类调用父类方法。

class T {
    synchronized void m(){
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("m end");
    }

    public static void main(String[] args) {
        new TT().m();
    }
}

class TT extends T{
    @Override
    synchronized void m(){
        System.out.println("child m start");
        super.m();
        System.out.println("chile m end");
    }
}
child m start
m start
m end
chile m end

volatile 

可见性

class T {
    /*volatile*/ boolean running=true;  //对比一下有无volatile的情况下,整个程序运行结果的区别
    void m(){
        System.out.println("m start");
        while (running){

        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        T t=new T();

        new Thread(t::m,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running=false;
    }
}

没加 volatile,结果,发现死循环,主线程改变false,发现没作用,因为t1线程感知不到running变化了

加了volatile,发现

volatile关键字,使一个变量在多个线程间可见
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必直到
使用volatile关键字,会让所有线程都读到该变量的修改值

在上面面的代码中,running是存在于堆内存的t对象中
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取
堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行

使用volatile,将会强制所有线程都去堆内存读取running的值
可以阅读这篇文章进行更深入的理解
Java内存模型 - 残雪余香 - 博客园

所有的变量都存储在主内存中,每条线程还有自己的工作内存,每次线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,

如果加了volatile,关键字,每次操作就会把修改后的变量刷回主内存,然后其他线程读取的时候发现缓存不一致,就会从主内存读取最新的。

原子性

一定要先看下这两篇文章:Java并发编程:volatile关键字解析对volatile不具有原子性的理解,再来看我的。

来看个例子,看起来是10个线程对inc加100000,理论结果为100000*10,但实际结果我们来运行下。

public class T2 {
    public volatile int inc = 0;

    public void increase() {
        for (int i = 0; i < 100000; i++) {
            inc++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t2.increase();
                    countDownLatch.countDown();
                }
            }).start();
        }

        //保证前面的线程都执行完
        countDownLatch.await();
        
        System.out.println(t2.inc);
    }
}

打印结果会发现,有可能恰好等于1000000,但是大多数情况都是小于1000000的数,这是为啥?

先来看下几个概念

原子操作

  • x = 10是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。  
  • x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
  • 只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

其实严格的说,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。”我们需要注意的是,这里的修改操作,是指的一个操作

从上面可以知道

  1. 案例中当A线程执行inc++ 时,有可能在inc从主存(内存条)读取inc最新的值为100后
  2. 突然被B线程抢占也执行inc++,也从主存读取inc最新的值为100到自己的工作内存,然后加1操作,写入到主存的inc,改为101
  3. 这时A线程抢回来,继续下一步原子操作,加1,这时在自己的工作内存inc值也为101,然后写回主存inc,主存也为101了
  4. 所以发现A,B两个线程进行inc++实际上只加了1,而不是表面看的加2.

所以,volatile并不保证原子性,所以想要成功可以吧volatile去掉,把方法加上synchronized(具备可见性和原子性),即可。

原理

  • 当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
  • 而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,所以就不会有可见性问题。
    • 对 volatile 变量进行写操作时,会在写操作后加一条 store 屏障指令,将工作内存中的共享变量刷新回主内存;
    • 对 volatile 变量进行读操作时,会在写操作后加一条 load 屏障指令,从主内存中读取共享变量;

AtomicInteger 

底层原理使用的是Unsafe类,Java中Unsafe类详解 - mickole - 博客园,看这篇

解决同样的问题的更高效的方法,使用AtomXXX类
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的

public class T2 {
    public AtomicInteger inc = new AtomicInteger(0);

    public void increase() {
        for (int i = 0; i < 100000; i++) {
            inc.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t2.increase();
                    countDownLatch.countDown();
                }
            }).start();
        }
        //保证前面的线程都执行完
        countDownLatch.await();

        System.out.println(t2.inc);
    }
}

这个方法还是在其他情况还是会有问题,比如 多加了个判断,可能刚进入if,就被别的线程抢了,所以,虽然AtomicInteger是原子操作,但是组合起来连续调用却不是原子操作,所以还是synchronized比较实在。

   public void increase() {
        for (int i = 0; i < 10000000; i++) {
            if (inc.get() < 1000000) {
                inc.incrementAndGet();
            }
        }
    }

其他方法 

compareAndSet(int expect,int update)

  • 当前值:new AtomicInteger(5)在堆内存中,就是在主内存中那个值,
  • 这个方法的作用就是比较并交换,如果预期值等于主内存的值,则以原子方式将该值设置为给定的更新值。

看个例子

public class Demo {
    public static void main(String[] args) {
        AtomicInteger ai = new AtomicInteger(5);
        // 获取真实值,并替换为相应的值
        boolean b = ai.compareAndSet(5, 2019);
        System.out.println(b + "  " + ai.get()); // true
        boolean b1 = ai.compareAndSet(5, 2020);
        System.out.println(b1 + "  " + ai.get()); // false
    }
}
true  2019
false  2019

 可以看出第一次比较并交换,发现主存中的值和预期值5相等,就修改为2019

第二次,发现主存中并不是5了,就不执行了。

练习

实现一个容器,提供两个方法:add,size
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束

public class MyContainer {
    //添加volatile,使t2能够得到通知
    volatile List<Object> list = new ArrayList<>(10);

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer c = new MyContainer();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    c.add(i);
                    System.out.println(i);
                }
            }
        },"t1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (c.size() == 5) {
                        System.out.println("已经5个了,结束");
                        break;
                    }
                }
            }
        },"t2").start();
    }
}
0
1
2
3
4
5
6
7
8
9
已经5个了,结束

这种写法,会发现,不是在4后面打印结束语句,而是t1执行完了,t2才继续执行,因为可能t2是知道等于5了,准备执行打印语句,但是这时cpu切换到t1,一直执行完了,才轮到t2。这就是出现这种情况的原因,当然也有可能出现在5到9之间任意一处。

怎么解决呢,这里使用synchronized,wait和notify可以做到,wait会释放锁,而notify不会释放锁

public class MyContainer {
    //添加volatile,使t2能够得到通知
    volatile List<Object> list = new ArrayList<>(10);

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer c = new MyContainer();
        //让两个线程共用一把锁
        final Object o=new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //不能用this,因为要使用notify和wait,需要用到对象,而两个线程this是不同的
                synchronized (o) {
                    System.out.println("t1开始");
                    o.notify();
                    for (int i = 0; i < 10; i++) {
                        if (i == 5) {
                            try {
                                o.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        c.add(i);
                        System.out.println(i);
                    }
                    System.out.println("t1结束");
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o) {
                    System.out.println("t2开始");
                    if (c.size() != 5) {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("t2结束");
                    o.notify();
                }
            }
        }, "t2").start();
    }
}
t1开始
0
1
2
3
4
t2开始
t2结束
5
6
7
8
9
t1结束

我们在逻辑方法上使用synchronized,加上锁,而且必须共用一把锁,主要思路分为2步,但有两种情况

第一种

  1. 假如t1线程先执行,获得o这把锁,先进来唤醒其他线程,为什么呢,看第二种情况,进入循环,当i=5当前线程就执行wait释放锁进入阻塞态。
  2. 轮到t2进来,发现size==5,就跳过if,打印结果,最后在通过o这把锁唤醒其他需要o的线程(这就是为什么要同一个锁的原因),t2生命结束,t1被唤醒就接着执行,然后生命周期结束。

第二种

  1. t2先执行,发现元素个数不为5,符合条件进入if语句,执行wait,为什么呢?因为不执行wait,那么这个线程生命周期就结束了,那t2就没事了,谁来监控,如果用while死循环来监控又太浪费cpu,所以当前线程释放锁并阻塞,等待t1线程需要它的时候唤醒它。
  2. t1进来,进入同步方法,先使用锁对象唤醒其他线程,然后循环到i==5,当前线程wait,释放锁,t2发现没人用这把锁了,就去用了,且回到上次执行的地方,继续往下执行

这种使用方式,基本就是一个线程使用先使用wait,在notify,另一个线程先notify,在wait,互相叫醒。

还有一种方式更加简便能实现上面的案例

  • 使用await和countdown方法替代wait和notify
  • CountDownLatch不涉及锁定,当count的值为零是当前线程继续运行
  • 当不涉及同步,只是涉及线程通信的时候,用synchronized+wait/notify就显得太重了
  • 这时应该考虑CountDownLacth/cyclicbarrier/semaphore
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  
public class MyContainer {
    //添加volatile,使t2能够得到通知
    volatile List<Object> list = new ArrayList<>(10);

    void add(Object o) {
        list.add(o);
    }

    int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer c = new MyContainer();
        //计数器为1
        CountDownLatch cd = new CountDownLatch(1);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2开始");
                try {
                    cd.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2结束");
            }
        }, "t2").start();

        //休眠1秒,先让t2执行,不然t1一次性执行完了,就没t2啥事了
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                //不能用this,因为要使用notify和wait,需要用到对象,而两个线程this是不同的

                System.out.println("t1开始");
                for (int i = 0; i < 10; i++) {
                    if (i == 5) {
                        cd.countDown();
                    }
                    c.add(i);
                    System.out.println(i);
                }
                System.out.println("t1结束");

            }
        }, "t1").start();
    }
}

ReentrantLock

可以看下这篇了解下ReentrantLock(重入锁)功能详解和应用演示 - takumiCX - 博客园

  • Reentrantlock用于替代synchronized
  • 需要注意的是,必须要手动释放锁
  • ReentrantLock可以实现公平锁,在创建ReentrantLock的时候通过传进参数true,就行。
  • 使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放
  • 使用reentrantlock可以进行“尝试锁定”trylock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待
  • 是自旋锁,一直循环等待查看是否锁没被其他线程占用,比synchronized竞争锁时被挂起消耗低

trylock()

  • 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
  • 如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
  • 使用trylock进行尝试锁定,不管锁定与否,方法都将继续执行
  • 可以根据trylock的返回值来判断是否锁定
  • 也可以指定trylock的时间,由于trylock(time)抛出异常,所以要注意unlock的处理,必须方法finally中

unlock()

  • 试图释放此锁。
  • 如果当前线程是此锁所有者,则将保持计数减 1。如果保持计数现在为 0,则释放该锁。
  • 如果当前线程没有保持此锁,则抛出 IllegalMonitorStateException。

来看下使用案例

public class T1 {
    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                TimeUnit.MILLISECONDS.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    void m2() {
        boolean islock=false;
        try {
            //在5秒内一直获取锁
            islock = lock.tryLock(5, TimeUnit.SECONDS);
            System.out.println("m2......" + islock);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //如果有锁才释放,不然没锁也运行,会抛异常
            if (islock) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        T1 t1 = new T1();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.m1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.m2();
            }
        }).start();
    }
}
0
1
2
3
4
m2......false
5
6
7
8
9

练习

写一个固定容量同步容器,拥有put和get方法
能够支持2个生产者线程以及10个消费者线程的阻塞调用


1.使用wait和notifyAll来实现

public class T1<T> {
    final private LinkedList<T> lists = new LinkedList<>();
    final private int MAX = 10;   //最多10个元素

    synchronized void put(T t) {
        while (lists.size() == MAX) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        lists.add(t);
        notifyAll();
    }

    synchronized T get() {
        //当没有元素的时候,停止消费者线程,释放锁,让生产者执行
        // 为啥不用if呢,因为,当生产者执行到notifyAll唤醒所有持有需要这把锁且在挂起状态的线程(就是消费者,也可能是生产者)
        //有可能另一个消费者线程B进入,不符合size==0,接着移除最后一个元素,刚好list元素个数为0
        // 然后这时又被切换到之前执行wait的消费者线程A,直接跳出if,执行removeLast(),可是已经没有元素了,在执行就会报错
        //所以要改成while,醒来的时候在进入循环判断一次是否为0
        //生产者也是如此,不能用if,要改成while,不然有可能元素会超过10个。
       while (lists.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       //移除最后一个元素
        T t = lists.removeLast();
        notifyAll();
        return t;
    }

    public static void main(String[] args) throws InterruptedException {
        T1<String> t1 = new T1<>();
        //这里消费总计50个,那么生产者也得50个,否则线程被wait,程序就无法停止
        //先启动消费者
       for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 5; j++) {
                        System.out.println(Thread.currentThread().getName()+"获取了"+t1.get());
                    }
                }
            }, "消费者" + i).start();
        }

        TimeUnit.SECONDS.sleep(2);

        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 25; j++) {
                        t1.put("c");
                        System.out.println(Thread.currentThread().getName()+"添加了"+"c");
                    }
                }
            }, "生产者" + i).start();
        }
    }
}

Effective Java说过:wait 基本和while一起使用

2.使用Lock和Condition来实现
对比两种方式,condition的方式可以更加精确的指定哪些线程被唤醒

public class T2<T> {
    final private LinkedList<T> lists = new LinkedList<>();
    final private int MAX = 10;   //最多10个元素
    private Lock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();


    void put(T t) {
        lock.lock();
        try {
            while (lists.size() == MAX) {
                try {
                    producer.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lists.add(t);
            consumer.signalAll();
        } finally {
            lock.unlock();
        }
    }

    T get() {
        lock.lock();
        T t;
        try {
            while (lists.size() == 0) {
                try {
                    consumer.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //移除最后一个元素
            t = lists.removeLast();
            producer.signalAll();
        } finally {
            lock.unlock();
        }
        return t;
    }

    public static void main(String[] args) throws InterruptedException {
        T2<String> t1 = new T2<>();
        //这里消费总计50个,那么生产者也得50个,否则线程被wait,程序就无法停止
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 5; j++) {
                        System.out.println(Thread.currentThread().getName() + "获取了" + t1.get());
                    }
                }
            }, "消费者" + i).start();
        }

        //先启动消费者
        TimeUnit.SECONDS.sleep(2);

        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 25; j++) {
                        t1.put("c");
                        System.out.println(Thread.currentThread().getName() + "添加了" + "c");
                    }
                }
            }, "生产者" + i).start();
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值