Java高并发

1、synchronized关键字:对某个对象加锁

任何线程进入都需要拿到obj的对象锁,如果对象已经被锁定,那就只能等待。

private Object obj = new Object();

public void test(){
    synchronized(obj){  //任何线程进入都需要拿到obj的对象锁,如果对象已经被锁定,那就只能等待。
        count++;
     }
}

2、synchronized关键字:锁定自身,给自己加锁。

任何线程进入都需要拿到this锁。

private Object obj = new Object();

public void test(){
    synchronized(this){  //锁定自身,相同的对象进入都会判断是否锁定。
        count++;
     }
}

3、synchronized关键字:如果一段代码在开始的时候就synchronized(this),结束的时候才释放锁。那么可以变形成下面的形式。

public synchronized void test()   这里依然锁住的是对象,不是代码!等同于synchronized(this),值得注意的是,这里如果静态的,即:public static synchronized void test()  。那么这里不等同synchronized(this),而是synchronized(类.class),因为静态可以直接调用,不需要对象!


public synchronized void test(){  //在代码块上加入synchronized

        count++;

}

4、简单的多线程不加锁

在没有加锁的情况下,多个线程在分时OS下运行,会出现一个线程没有执行完毕,另外一个线程进入的情况,导致数据失去准确性,解决只需要run()代码块中加入synchronized,一个synchronized代码块是一个原子操作。

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

5、一个非加锁的方法和一个加锁的方法可以同时并发执行,互不影响

加锁的方法会去申请对象锁,不加锁的方法不会去申请对象锁,两者不影响。

public class T {
    
    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() {
        System.out.println(Thread.currentThread().getName() + " m2 start");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2 end");
    }

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

6、对读不加锁出现的脏读现象

一般我们认为为了保证数据的安全,我们可以对写入操作加锁,而忘记了对读操作加锁,这样会出现脏读的显现,因为在实际操作过程中,写的操作时间周期很长,而在写的过程中,程序可以进行其他操作,比如读操作,这样会出现读取的数据不完整,从而数据失效。

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();
        System.out.println(a.getBalance()); // 0.0 
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance()); // 100.0
    }
}

7、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时,不会发生死锁
    }
}

8、子类的同步方法,可以调用父类的同步方法

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 ");
    }
}

9、线程出现异常会释放锁

线程异常会释放锁,会导致异常还没有处理完成的数据让其他线程访问,很容易出现数据异常,如果不想这样就需要try catch

/**
 * 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();
    }

}

 8、volatile 关键字,使一个变量在多个线程间可见

能用volatile就不用加锁

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

}

9、volatile只能保证可见性,不能保证原子性

volatile不能保证原子性,所以在使用volatile时,CPU不会去检查数据是否修改,只会把拿到的数据进行更新。

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);
        
        // 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 代表此类中的所有方法都是原子操作,并且可以保证可见性)
 */

10、但是如果锁定对象变成另一个对象,则锁定的对象发生变化

因为锁定的对象是堆内存中的对象,如果对内存中的对象发生改变,那么锁就会自动释放。

/**
 * 锁定某个对象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();
    }

}

11、不要以字符串常量作为锁定对象

/**
 * 不要以字符串常量作为锁定对象
 * 在下面的例子中, m1和m2其实是锁定的同一对象
 * 这种情况下,还会可能与其他类库发生死锁,比如某类库中也锁定了字符串 "Hello"
 * 但是无法确认源码的具***置,所以两个 "Hello" 将会造成死锁
 * 因为你的程序和你用的类库无意间使用了同意把锁
 */
public class T {

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

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

 

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务