Java并发编程基础--马士兵1

synchronized 关键字 对某个对象加锁

public class T {
    private int count = 0;
    private final Object lock = new Object();
    
    public void m() {
        synchronized (lock) { // 任何线程要执行下面的代码,都必须先拿到lock锁,
             //锁信息记录在堆内存对象中的,不是在栈引用中
             // 如果lock已经被锁定,其他线程再进入时,就会进行阻塞等待
            // 所以 synchronized 是互斥锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        // 当代码块执行完毕后,锁就会被释放,然后被其他线程获取
    }
}
/**
 * synchronized 关键字 对this加锁
 * 每次使用锁都要newObject,比较麻烦,可以使用this代替object锁
 */
public class T {
    private int count = 10;
    
    public void m() {
        synchronized (this) {    // 任何线程要执行下面的代码,
                          //必须先拿到this锁
                        // synchronized 锁定的不是代码块,而是 this 对象
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}
/**
 * synchronized 关键字
 * synchronized 方法
 */
public class T {
    private int count = 10;

    public synchronized void m() {     // 等同于 synchronized (this) { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

锁定静态方法,其实锁定的是 java.lang.Class 对象。

/**
 * synchronized 关键字
 * synchronized 静态方法
 * 锁定静态方法,其实锁定的是 java.lang.Class 对象。 静态方法没有this引用的
 */
public class T {

    private static int count = 10;

    public static synchronized void m() { // 等同于 synchronized (T.class) {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}
public class T implements Runnable{

    private int count = 10;
    
    @Override
    public /*synchronized*/ void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void main(String[] args) {
        T t = new T();
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();
        }
    }
}

/*
某次运行结果:
Thread-0 count = 7
Thread-4 count = 5
Thread-3 count = 6
Thread-2 count = 7
Thread-1 count = 7

线程重入的问题(线程执行过程中,被其他线程打断),因为 count-- + " count = " + count 不是原子操作
解决办法,保证操作原子性,加上 synchronized 关键字
 */

同步方法和非同步方法是否可以同时调用?答:肯定可以

public class T {
    private int count = 10;
    
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    
    public void m2() {
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+" m2 running");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    }
}

**
对业务写方法加锁,而对业务读方法不加锁,容易出现 脏读问题
因为,在执行写的过程中,因为读操作没有加锁,
所以读会读取到写未改完的脏数据
解决办法,给读写都加锁
**

public class Account {
    String name; // 银行账户名称
    double balance; // 银行账余额

    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public /*synchronized*/ double getBalance() {
        return this.balance;
    }

    public static void main(String[] args) {
        Account a = new Account();
        new Thread(() -> a.set("张三", 100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance()); 		// 0.0  double初始化的值0.0 
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance()); 	// 100.0
    }
}

synchronized 是可重入锁. 即一个同步方法可以调用另外一个同步方法,
一个线程已经拥有某个对象的锁,再次申请时仍然会得到该对象的锁

public class T {

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

    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" m2"); // 这句话会打印,调用m2时,不会发生死锁
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1).start();
    }
}

synchronized 是可重入锁, 子类调用父类的同步方法,也是可重入的
下面例子中锁定的是同一个对象

public 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) {
        TT tt = new TT();
        tt.m();
    }
}

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

synchronized 代码块中,如果发生异常,锁会被释放
在并发处理过程中,有异常要多加小心,不然可能发生数据不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一资源,
这时如果异常处理不合适, 第一个线程抛出异常,锁会内释放。
其他线程就会进入同步代码区,有可能访问到异常产生的数据,
第一个线程处理到一半的数据。
因此要非常小心处理同步业务员逻辑中的异常。

public class T {
    int count = 0;
    
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while (true) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count=" + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 5) {  // 当count == 5 时,synchronized代码块会抛出异常
                int i = 1 / 0; 
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        };
        new Thread(r, "t1").start(); // 执行到第5秒时,抛出 ArithmeticException 
        // 如果抛出异常后,t2 会继续执行,就代表t2拿到了锁,即t1在抛出异常后释放了锁
        
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(r, "t2").start();
    }

}

volatile 关键字,使一个变量在多个线程间可见
cn: 透明的,临时的
JMM(Java Memory Model):
在JMM中,所有对象以及信息都存放在主内存中(包含堆、栈)
而每个线程都有自己的独立空间,存储了需要用到的变量的副本,
线程对共享变量的操作,都会在自己的工作内存中进行,然后同步给主内存
下面的代码中,running 是位于堆内存中的t对象的
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,
在运行过程中直接使用这个copy,并不会每次都会去读取堆内存,
这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值。
加了volatile,不是线程每次都去主动去读,而是修改了值后,通知各个线程失效,重新读取

public class T {
    /*volatile*/ boolean running = true;   // 对比有无volatile的情况下,
 										   //整个程序运行结果的区别
    void m() {
        System.out.println(" m start ");
        while (running) { 			// 直到主线程将running设置为false,
        									//T线程才会退出
            							// 在while中加入一些语句,
            							//可见性问题可能就会消失,
            							//这是因为加入语句后,
            							//CPU可能就会出现空闲,
            							//然后就会同步主内存中的内容到工作内存
          							  // 所以,可见性问题可能会消失
            /*try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
        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 关键字,使一个变量在多个线程间可见
volatile并不能保证多个线程共同修改running变量所带来的不一致的问题,
也就是说volatile不能替代synchronized,
即 volatile只能保证可见性,不能保证原子性

public class T {
    volatile int count = 0;
    /*AtomicInteger count = new AtomicInteger(0);*/
    
    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
            /*count.incrementAndGet();*/
        }
    }

    public static void main(String[] args) {
        // 创建一个10个线程的list,执行任务皆是 m方法
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "t-" + i));
        }
        
        // 启动这10个线程
       // threads.forEach(Thread::start);
        threads.forEach((o)->o.start());
        
        // join 到主线程,防止主线程先行结束
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 10个线程,每个线程执行10000次,结果应为 100000
        System.out.println(t.count);  // 所得结果并不为 100000,说明volatile 不保证原子性
    }

}

/*
解决方案:

  1. 在方法上加上synchronized即可,synchronized既保证可见性,又保证原子性
  2. 使用AtomicInteger代替int(AtomicXXX 代表此类中的所有方法都是原子操作,并且可以保证可见性)
    */

synchronized 优化,同步代码块中的语句越少越好。比较m1和m2

public class T {
    int count = 0;
    
    synchronized void m1() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 业务逻辑中,只有下面这句代码需要 sync, 这时不应该给整个方法上锁
        count++;

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 业务逻辑中,只有下面这句需要 sync,这时不应该给整个方法上锁
        // 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized (this) {
            count++;
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

** 锁定某个对象o,如果o属性发生变化,不影响锁的使用
但是如果o变成另一个对象,则锁定的对象发生变化,
所以锁对象通常要设置为 final类型,保证引用不可以变**

public class T {
     Object o = new Object();
     
    void m() {
        synchronized (o) {
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "线程1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread thread2 = new Thread(t::m, "线程2");
        t.o = new Object(); // 改变锁引用, 线程2也有机会运行,否则一直都是线程1 运行
        thread2.start();
    }
}

不要以字符串常量作为锁定对象,在下面的例子中, m1和m2其实是锁定的同一对象,这种情况下,还会可能与其他类库发生死锁,比如某类库中也锁定了字符串 “Hello”, 但是无法确认源码的具体位置,所以两个 “Hello” 将会造成死锁. 因为你的程序和你用的类库无意间使用了同一把锁。 原先jetty出现过类似的bug

public class T {
    String s1 = "Hello";
    String s2 = "Hello";
    
    void m1() {
        synchronized (s1) {
            
        }
    }

    void m2() {
        synchronized (s2) {
       
        }
    }

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

public class MyContainer1 {

    private List<Object> list = new ArrayList<>();

    public void add(Object ele) {
        list.add(ele);
    }

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

    public static void main(String[] args) {

        MyContainer1 container = new MyContainer1();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                container.add(new Object());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("add " + i);
            }
        }, "t1").start();

        new Thread(() -> {
            while (true) {
                if (container.size() == 5) {
                    break;
                }
            }
            System.out.println("监测到容器长度为5,线程2立即退出");
        }, "t2").start();

    }

}

此种方法是一种错误的实现:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
.... t2 一直在运行,永远不结束


这是因为 container 对象的可见性问题
public class MyContainer2 {

    private volatile List<Object> list = new ArrayList<>();

    public void add(Object ele) {
        list.add(ele);
    }

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

    public static void main(String[] args) {

        MyContainer2 container = new MyContainer2();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                container.add(new Object());
                System.out.println("add " + i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        }, "t1").start();

        new Thread(() -> {
            while (true) {
                if (container.size() == 5) {
                    break;
                }
            }
            System.out.println("监测到容器长度为5,线程2立即退出");
        }, "t2").start();

    }

}
添加 volatile ,使list发生变化时,主动通知其他线程,更新工作空间

上述代码,共有以下几个问题:
1. 不够精确,当container.size == 5 还未执行break时,有可能被其他线程抢占;
2. 或者 container.add() 之后,还未打印,就被 t2 判断size为5 直接退出了
3. 损耗性能,t2 线程,一直在走while循环,很浪费性能
这个程序一定要先启动t2线程
public class MyContainer3 {

    private List<Object> list = new ArrayList<>();

    public void add(Object ele) {
        list.add(ele);
    }

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

    public static void main(String[] args) {

        MyContainer3 container = new MyContainer3();

        final Object lock = new Object();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 启动");
                if (container.size() != 5) {		//这里不需要while  
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("监测到容器长度为5,线程2立即退出");
              
                //t2自己结束之前,通知t1继续执行,t2结束了,锁也就释放了。
                lock.notify();
            }
        }, "t2").start();

        // 先启动t2线程,让t2线程进入等待状态
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    container.add(new Object());
                    System.out.println("add " + i);
                    // 当长度为5时,通知 t2 进行退出
                    if (container.size() == 5) {
                        lock.notify();			 // notify 不会释放锁,即便通知t2,t2也获取不到锁
                        
                        // 可以再wait一下,将锁释放,再让等待t2通知自己 t1继续执行
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
    }
}

使用wait和notify , 必须锁定对象,wait和notify是调用被锁定对象的wait和notify。
xxx.wait()  当前线程释放锁,并且进入等待状态。别的线程可以进来。
xxx.notify()  只有其他线程调用这个对象的notify方法,
               会唤醒正在这个对象上等待的某一个线程( CPU 随机唤醒)。
xxx.notifyAll方法,会唤醒这个对象上所有的等待线程。
wait()与notify() 方法的调用必须在同步代码块中
wait会释放锁,notify不会释放锁

锁定对象a,调用a.wait() 方法,当前线程就会进入等待状态,然后释放锁。
当某线程调用 a.notify() / a.notifyAll(), 叫醒在a对象等待的所有线程

使用CountDownLatch实现(最简单的方式)
Latch:门闩 使用Latch替代 wait notify来进行通信
好处是,通信简单,同时也可以指定等待时间
使用await和countDown 方法替代 wait 和 notify
CountDownLatch不涉及锁定,当count值为0时,当前线程继续运行
当不涉及同步,只涉及线程通信的时候,用synchronized + wait + notify 就显得太重了

public class MyContainer5 {
    private volatile List<Object> list = new ArrayList<>();

    public void add(Object ele) {
        list.add(ele);
    }

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

    public static void main(String[] args) {
        MyContainer5 container = new MyContainer5();

        // Count down 往下数  Latch 门闩
        // 门闩不能保证可见性,不是一种同步方式,只是一种线程通信方式,保证不了可见性
        // 门闩的等待,不会持有任何锁
        CountDownLatch latch = new CountDownLatch(1);

        new Thread(() -> {
            System.out.println("t2 启动");
            if (container.size() != 5) {
                try {
                    latch.await();
                    // 指定等待时间
                    //latch.await(5000, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("监测到容器长度为5,线程2立即退出");
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                container.add(new Object());
                System.out.println("add " + i);
                // 当长度为5时,撤掉一个门闩,此时门闩为0,门会打开,即t2会执行
                if (container.size() == 5) {
                    latch.countDown();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值