并发编程demo记录demo1-18

马士兵高并发编程系列记录

https://www.bilibili.com/video/av11076511/?spm_id_from=333.788.videocard.4

demo1

public class T1 {
	private int count = 10;
	private Object o = new Object(); // o可以理解为监视器,谁要指向下面那个代码,先到o那里给o加锁,不然不能指向下面的代码
    //o在堆内存new出来一个对象,对象可能有一些属性和方法,

	public void m() {
		synchronized(o) {
		    //任何线程要执行下面的代码,必须先拿到o的锁(是堆内存new出来一个对象,而不是o的引用)
            //申请锁的时候,信息是记录在堆内存的对象
			count--;
			System.out.println(Thread.currentThread().getName() + " count = " + count);
		}
	}
}

!!!!synchronized不是锁定整段代码,是执行代码的时候锁定当前对象
synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
static synchronized方法也相当于全局锁,相当于锁住了代码段。

demo2

public class T2 {
    private int count = 10;
    public void m() {
        synchronized(this) { //任何线程要执行下面的代码,必须先拿到this(自身)的锁
            //synchronized锁定的是对象
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

demo3

public class T3 {
    private int count = 10;
    public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

T2与T3等同

demo4

public class T4 {
	private static int count = 10;
	public synchronized static void m() { // static 静态,这里等同于synchronized(T.class)
		count--;
		System.out.println(Thread.currentThread().getName() + " count = " + count);
	}
	public static void mm() {
		synchronized(T4.class) {
		// T4.class  是Class类的一个对象
		//考虑一下这里写synchronized(this)是否可以? 不可以 静态的属性和方法是不需要new出对象的,没有对象所以没有this,所以锁定的是该类的class对象
			count --;
		}
	}
}

demo5

public class T5 implements Runnable {
    private int count = 10;

    /*
    public void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    不加synchronized可能会出现数据不一致的问题,比如结果为7 5 6 7 7
    线程重入:
    线程1在运行到count--还没有执行下一句的打印语句的时候被线程2打断,
    线程2在运行到count--还没有执行下一句的打印语句的时候被线程3打断,线程3执行count--以及打印语句打印出7。。。

    要想得到想要的结果9,8,7,6,5
    加锁synchronized,某个线程在执行这段代码的时候不能被打断
    如下面的run()方法
    */


    //一个synchronized代码块,相当于一个原子操作,说明该段代码不可分,某个线程在执行这段代码的时候不能被打断,只有这个线程执行完,其他线程才能执行这段代码
    public synchronized void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void main(String[] args) {
        T5 t = new T5();//只new了一个对象,好多线程共同访问这一个对象,并不是每个线程都new一个对象
        //栈内存里有一个t,指向堆内存new一个T对象,
        for(int i=0; i<5; i++) {
            //开启了5个线程,每个线程都访问t里面的run方法,访问的count都是堆内存new一个T对象里面记录的count
            new Thread(t, "THREAD" + i).start();//创建线程并命名,启动线程
        }
    }
}

demo6

与上一个相同

public class T6 implements Runnable {
	private int count = 10;
	public synchronized void run() {
		count--;
		System.out.println(Thread.currentThread().getName() + " count = " + count);
	}
	public static void main(String[] args) {

		for(int i=0; i<5; i++) {
			T6 t = new T6();
			new Thread(t, "THREAD" + i).start();
		}
	}
}

demo7

/**
 * 同步和非同步方法是否可以同时调用?  可以
 * @author mashibing
 */
public class T7 {
    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) {
        T7 t = new T7();
        //lamada表达式 java8特性
		new Thread(()->t.m1(), "t1").start();
		new Thread(()->t.m2(), "t2").start();
		//synchronized是重入锁
        //只有synchronized这样的方法在执行的时候才需要这把锁,其他方法不需要这把锁

//        new Thread(t::m1, "t1").start();
//        new Thread(t::m2, "t2").start();
        //这两句与上面一样

		/*
		new Thread(t::m1 相当于
		new Thread(new Runnable() {
			@Override
			public void run() {
				t.m1();
			}

		});
		*/
    }
}

demo8

/**
 * 对业务写方法加锁
 * 对业务读方法不加锁
 * 容易产生脏读问题(dirtyRead)
 */

import java.util.concurrent.TimeUnit;

public class Account {
    String name;
    double balance;

    public synchronized void set(String name, double balance) {
        this.name = name;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        this.balance = balance;
    }
    public double getBalance(String name) {
        return this.balance;
    }


    public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan", 100.0)).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance("zhangsan"));
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(a.getBalance("zhangsan"));
    }
}

/*
结果是0.0 100.0
对写进行加锁,对读没有加锁,可能出现读到在写的过程中还没有完成的数据,造成脏读
脏读(一个事务读取到了另外一个事务没有提交的数据 )
写线程加锁,读线程没有加锁,加入A线程在写,BC线程在读,B读的是写之前的数据(sleep两秒钟之前),C读的是写之后的数据(sleep两秒钟之后),所以读出的结果不一致
set函数中sleep两秒钟是因为放大线程之前的时间差
将sleep代码注释掉后,两次读出的数据一致,因为中间没有sleep马上账户余额设置为100
//try {
//        Thread.sleep(2000);
//    } catch (InterruptedException e) {
//        e.printStackTrace();
//    }


问题的解决:对读也进行加锁
public synchronized double getBalance(String name) {
    return this.balance;
}
*/

demo9

一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
也就是说synchronized获得的锁是可重入的 (获得锁之后还可以再获得一遍)

import java.util.concurrent.TimeUnit;  
public class T9 {
    //同一个线程是可以的,因为是重入锁
    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");
    }
}

demo10

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

import java.util.concurrent.TimeUnit;
public class T10 {
    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 T10 {
    @Override
    synchronized void m() {
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}

demo11

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

import java.util.concurrent.TimeUnit;
public class T11 {
    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) {
                int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T11 t = new T11();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t.m();
            }

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

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

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

demo12

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

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

使用volatile,将会强制所有线程都去堆内存中读取running的值

可以阅读这篇文章进行更深入的理解
http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
synchronized既有可见性又保持原子性,volatile只是保持可见性

volatile 当改了主内存中的值后会通知其他线程重新读一下
过期缓存通知
不加上volatile,线程之间不可见

当两个线程共同访问的变量要加volatile,保持线程之间的可见性,不然要加synchronized,synchronized太重了,效率比较低,volatile效率比较高
能用volatile就不要加锁,程序的并发性可以提高很多

import java.util.concurrent.TimeUnit;

public class T12 {
    /*volatile*/ boolean running = true;
    //对比一下有无volatile的情况下,整个程序运行结果的区别
    //不加 m start ,加上 m start  m end!
    void m() {
        System.out.println("m start");
        while(running) {
			/*
			// 不加sleep语句,cpu特别忙,不会去主内存刷新running的值,加上sleep,可能cpu有空闲,有可能回去主内存刷新一下running的值
			// 当两个线程共同访问的变量要加volatile,保持线程之间的可见性,不然要加synchronized,synchronized太重了,效率比较低,volatile效率比较高
			//能用volatile就不要加锁,程序的并发性可以提高很多
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			*/
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        T12 t = new T12();
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;
    }
}

demo13&14

volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
synchronized既有可见性又保持原子性,
volatile只是保持可见性(一个值修改后另一个可见)
链接:https://www.zhihu.com/question/49656589/answer/117826278
volatile 只能保证 “可见性”,不能保证 “原子性”。count++; 这条语句由3条指令组成:
(1)将 count 的值从内存加载到 cpu 的某个寄存器r
(2)将 寄存器r 的值 +1,结果存放在 寄存器s
(3)将 寄存器s 中的值写回内存所以,如果有多个线程同时在执行 count++;,

在某个线程执行完第(3)步之前,其它线程是看不到它的执行结果的。在没有 volatile 的时候,执行完 count++;,
执行结果其实是写到CPU缓存中,没有马上写回到内存中,后续在某些情况下(比如CPU缓存不够用)再将CPU缓存中的值flush到内存。
正因为没有马上写到内存,所以不能保证其它线程可以及时见到执行的结果。在有 volatile 的时候,执行完 count++;,
执行结果写到CPU缓存中,并且同时写回到内存,因为已经写回内存了,所以可以保证其它线程马上看到执行的结果。
但是,volatile 并没有保证原子性,在某个线程执行(1)(2)(3)的时候,volatile 并没有锁定 count 的值,
也就是并不能阻塞其他线程也执行(1)(2)(3)。可能有两个线程同时执行(1),所以(2)计算出来一样的结果,然后(3)存回的也是同一个值。

import java.util.ArrayList;
import java.util.List;

public class T13 {
    volatile int count = 0;
    void m() {
        for(int i=0; i<10000; i++)
            count++;
    }
    public static void main(String[] args) {
        T13 t = new T13();
        List threads = new ArrayList();
        for(int i=0; i<10; i++) {
            /*十个线程共同访问一个变量count(加volatile,保证了可见性),每个线程往上加10,000个数,正常应该是100,000
            A线程现在count是100,B读走现在的值是100,B加一写回去是101,A加一写回去也是101,将B的101覆盖了一遍,
            此时假如C去读取,读取到101(可见性),加入C去加1,写回去的时候不去检查
            */
            threads.add(new Thread(t::m, "thread-"+i));
        }

        threads.forEach((o)->o.start());
        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

解决 去掉volatile,在方法前加synchronized.(也是第14个例子)

import java.util.ArrayList;
import java.util.List;
public class T13_modify {
    int count = 0;
    synchronized void m() {
        for(int i=0; i<10000; i++)
            count++;
    }
    public static void main(String[] args) {
        T13_modify t = new T13_modify();
        List threads = new ArrayList();
        for(int i=0; i<10; i++) {
            threads.add(new Thread(t::m, "thread-"+i));
        }

        threads.forEach((o)->o.start());
        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

demo15

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T15 {
    AtomicInteger count = new AtomicInteger(0);
    void m() {
        for (int i = 0; i < 10000; i++)
            //if count.get() < 1000
            count.incrementAndGet(); //替代count++(不具有原子性);  incrementAndGet是原子方法,效率比synchronized高很多
    }
    public static void main(String[] args) {
        T15 t = new T15();
        List threads = new ArrayList();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));//创建线程
        }

        threads.forEach((o) -> o.start());//JDK1.8新特性 启动线程
        threads.forEach((o) -> {
            try {
                o.join();//等线程执行完毕之后才执行主线程main
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T15_add {
    AtomicInteger count = new AtomicInteger(0);
    void m() {
        for (int i = 0; i < 10000; i++) {
            if (count.get() < 1000)  //count.get() 具有原子性
                count.incrementAndGet();// count.incrementAndGet() 具有原子性
            /*
            count.get() 具有原子性,count.incrementAndGet() 具有原子性
            但是在两句话的中间没有原子性  加入A线程读取到999,判断count<1000还没有加一,但是B线程读取到999,判断count<1000加1到1000了,
            此时A线程继续执行(已经判断完)加1结果为1001
            */
        }
    }
    public static void main(String[] args) {
        T15_add t = new T15_add();
        List threads = new ArrayList();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

demo16

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

import java.util.concurrent.TimeUnit;
public class T16 {
    int count = 0;
    synchronized void m1() {//整个加synchronized 粗粒度的锁
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        count ++;

        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized(this) {
            count ++;
        }
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

demo17

锁定某对象o,如果o的属性发生改变,不影响锁的使用
但是如果o变成另外一个对象,则锁定的对象发生改变
应该避免将锁定对象的引用变成另外的对象

import java.util.concurrent.TimeUnit;
public class T17 {
    Object o = new Object();// 栈内有小内存,指向堆中对象真正存放的地方,锁定某对象o是锁定真正的对象(堆内存中)而非引用(栈内存中)
    void m() {
        synchronized(o) {
            while(true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T17 t = new T17();
        //启动第一个线程
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建第二个线程
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会

        t2.start();

    }
}

运行结果:
t1
t1
t1
t2
t1
t2等等
分析结果:相当于两个在堆内存里的对象都有锁,两个线程各获取两个对象上的锁,互相不影响

demo18

不要以字符串常量作为锁定对象(锁的不是引用)
在下面的例子中,m1和m2其实锁定的是同一个对象,所以下面程序看似是两把锁,其实是同一把锁。
这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
因为你的程序和你用到的类库不经意间使用了同一把锁

jetty出现过这个bug

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

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

        }
    }
}

你可能感兴趣的:(并发编程,java)