[Java并发]の其二

13.烧水泡茶

	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread() {
			public void run() {
				System.out.println("洗水壶");
				try {
					sleep(1);
				} catch	 (InterruptedException e) {
					e.printStackTrace();
				}
				
				System.out.println("烧开水");
				try {
					sleep(15);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				System.out.println("洗茶壶");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				System.out.println("洗茶杯");
				try {
					sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				System.out.println("拿茶叶");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				try {
					t1.join();//等待t1执行完,等水烧好.
				} catch (InterruptedException e) {
					 e.printStackTrace();
				}
				System.out.println("pao cha");
			}
		};
		t1.start();
		t2.start();
	}

这种接法的缺陷:

 上面的解法是等t1水烧开后,t2泡茶,如果反过来要实现t1等t2拿过来的茶叶,由t1来进行泡茶呢? 没有满足第二种情况.

上面的线程各自执行,如果要模拟t1把水壶给t2泡茶,或者模拟t2把茶叶交给t1泡茶.

后面要学习的内容有:

  • 共享问题.
  • synchronized
  • 线程安全分析
  • Monitor
  • wait/notify
  • 线程状态转换
  • 活跃性
  • Lock

14.临界区

  • 一个程序运行多个线程本身是没有问题的.
  • 问题出在多个线程访问共享资源
  • 多个线程读共享资源其实也没有问题.
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题.
  • 一段代码块内如果存在对共享资源的多线程读写操作,这称这段代码块为临界区.

14.1竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件.

例如:t1,t2线程交替执行,有可能会导致结果不正确.

static int count=0;
	
	public static void main(String[] args) throws InterruptedException {
		
		Thread t1=new Thread() {
			public void run() {
				for(int i=0;i<5000;++i) {
					count++;
				}
			}
		};
		
		Thread t2=new Thread() {
			public void run() {
				for(int i=0;i<5000;++i) {
					count--;
				}
			}
		};
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(count);
	}

14.2synchronized解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的.

  • 阻塞式的解决方案,synchronized,Lock.
  • 非阻塞式的解决方案:原子变量.

本次课使用阻塞式的解决方案,:synchronized,来解决以上问题,即俗称的[对象锁],它采用了互斥的方式让同一时刻至多只有一个线程能持有[对象锁].其他线程再想获取这个[对象锁]时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换.

synchronized语法:

synchronized(对象){   临界区    }

14.3关于synchronized的思考

 synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的.不会被线程的切换所打断,

为了加深理解.请细品一下3个问题.

  • 如果把synchronized(obj)放在for循环之外呢,如何理解?
         Thread t1=new Thread() {
			public void run() {
                   synchronized (Lock) {
				   for(int i=0;i<5000;++i)
						count++;
				}
			}
		};

synchronized的作用就是使得保护的临界区原子化,如果synchronized放在for的外面,那么就意味着for循环内的代码具有了原子性,在执行不完for中的代码前,是不会解锁的. ----强调原子性

  • 如果t1 synchronized(obj1)而t2 synchronized(obj2) 会怎样运作?

对象锁不同,要保护共享资源需要多个线程锁住的是同一个对象,--强调锁对象

  • 如果t1 synchronized(obj) 而t2并没有加锁,会怎么样?

t2如果不加锁,等于没有限制,,就不会去获取锁对象,也就不会被阻塞(Blocked),就可以所以的操作共享资源.

14.4锁对象---面向对象改进

public class Test15 {
	
	static int count=0;
	//static Object Lock = new Object();
	public static void main(String[] args) throws InterruptedException {
		Room room = new Room();
		
		Thread t1=new Thread() {
			public void run() {
				for(int i=0;i<5000;++i) {
					 room.increament();
				}
			}
		};
		
		Thread t2=new Thread() {
			public void run() {
				for(int i=0;i<5000;++i) {
					 room.decreament();
				}
			}
		};
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(room.getCount());
	}

}


class Room{
	
	private int count=0;
	
	public void increament() {
		synchronized (this) {
			count++;
		}
	}
	
	public void decreament() {
		synchronized (this) {
			count--;
		}
	}
	
	public int getCount() {
		synchronized (this) {
			return count;
		}
	}
}

14.5方法上的synchronized

加在非静态方法上的synchronized

    public synchronized void test() {
		
	}

等价于

   public void test1() {
		synchronized (this) {

		}
	}
	

加在静态方法上的synchronized

    public synchronized static void test2() {
		
	}

等价于

    public static void test2() {
		synchronized (Test16.class) {
			
		}
	}

注意:synchronized加在了非静态方法上了,并不是意味着将方法锁住了,而是方法里面的对象(this)

加在了静态方法上等价于是锁住了类对象.

不加synchronized的方法就好比不遵守规则的人,不老实排队(去翻窗子进去)

15.线程八锁.

其实主要考察的就是synchronized锁住的是哪个对象?

如果是相同的对象则会存在互斥,.如果是不同的对象则不存在互斥,直接按照线程并发执行处理..

15.1第一锁.

public class Lock1 {
	public static void main(String[] args) {
		Number n1 = new Number();
		//启动线程1
		new Thread() {
			public void run() {
				n1.a();
			}
		}.start();
		//启动线程2 
		new Thread() {
			public void run() {
				n1.b();
			}
		}.start();;
	}
}

class Number{
	public synchronized void a() {
		System.out.println("1");
	}
	public synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:首先synchronized锁的是Number.class的成员方法:a(),b();查看锁对象是不是this.
  • 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.
  • 因此:CPU调度先调度哪个线程,哪个线程就先执行.
  • 执行结果:有两种情况
  • 12(因为第一个线程先启动,所以出现,12的概率大.)或者21

15.2第二锁

public class Lock2 {
	public static void main(String[] args) {
		Number2 n1 = new Number2();
		//启动线程1
		new Thread() {
			public void run() {
				System.out.println("线程1启动");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				n1.a();
			}
		}.start();
		//启动线程2 
		new Thread() {
			public void run() {
				System.out.println("线程2启动");
				n1.b();
			}
		}.start();;
	}
}

class Number2{
	public synchronized void a() {
		System.out.println("1");
	}
	public synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:首先synchronized锁的是Number2.class的成员方法:a(),b();查看锁对象是不是this.
  • 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.
  • 因此:CPU调度先调度哪个线程,哪个线程就先执行.
  • 执行结果:有两种情况
  • 12(因为第一个线程先启动,所以出现,12的概率大.)或者21
  • sleep()方法并不能释放锁.

15.3第三锁

public class Lock3 {
	public static void main(String[] args) {
		Number3 n1 = new Number3();
		//启动线程1
		new Thread() {
			public void run() {
				System.out.println("线程1启动");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				n1.a();
			}
		}.start();
		//启动线程2 
		new Thread() {
			public void run() {
				System.out.println("线程2启动");
				n1.b();
			}
		}.start();
		//启动线程3
		new Thread() {
			public void run() {
				System.out.println("线程3启动");
				n1.c();
			}
		}.start();
	}
}

class Number3{
	public synchronized void a() {
		System.out.println("1");
	}
	public synchronized void b() {
		System.out.println("2");
	}
	public  void c() {
		System.out.println("3");
	}
}
  • 分析:首先synchronized锁的是Number3.class的成员方法:a(),b();查看锁对象是不是this   c()方法无锁.
  • 查看:两个线程都是通过n1来进行调用的,-->表明this相同(即当前的锁对象相同)---->>是存在互斥关系的.但是线程3没有锁.
  •  condition1:如果线程1首先拿到了时间片,t2阻塞,t2在等待t1释放锁.线程1进入一秒的睡眠后,因为线程3并没有加锁,线程3可以就可以争取到了时间片,进行输出.
  • 结果:3 12
  • condition2:如果是线程2首先拿到了时间片,线程1会阻塞,但是线程3无锁,那么意味着,线程3可以和线程2竞争使用权,
  1. 线程2拿到使用权并输出,然后是线程3,线程2执行完毕,无论是线程3还是线程1抢到时间片,但是因为线程1会休眠,所以线程3总是在线程1之前输出的.即:321,231.

15.4第四锁

public class Lock4 {
	public static void main(String[] args) {
		Number4 n1 = new Number4();
		Number4 n2 = new Number4();
		//启动线程1
		new Thread() {
			public void run() {
				System.out.println("线程1启动");
				try {
					sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				n1.a();
			}
		}.start();
		//启动线程2 
		new Thread() {
			public void run() {
				System.out.println("线程2启动");
				n2.b();
			}
		}.start();
	}
}

class Number4{
	public synchronized void a() {
		System.out.println("1");
	}
	public synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:首先两个线程获取到的对象锁都不是同一个,即不存在冲突问题.按照普通并发处理.
  • 因为线程1在输出前总要睡眠一秒.所以输出结果:总是.21.

15.5第五锁

public class Lock5 {
	public static void main(String[] args) {
		 Number5 n1 = new Number5();
		 //线程1
		 new Thread() { public void run() {try {
			n1.a();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}}}.start();
		//线程2
		 new Thread() { public void run() {n1.b();}}.start();;
	}
}

class Number5{
	public static synchronized void a() throws InterruptedException {
		Thread.sleep(1000);
		System.out.println("1");
	}
	public synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:因为两个线程一直执行的是静态方法,一个是成员方法.锁对象不同,因此无互斥.
  • 输出:2 1

15.6第六锁

public class Lock6 {
	public static void main(String[] args) {
		 Number6 n1 = new Number6();
		 //线程1
		 new Thread() { public void run() {try {
			n1.a();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}}}.start();
		//线程2
		 new Thread() { public void run() {n1.b();}}.start();;
	}
}

class Number6{
	public static synchronized void a() throws InterruptedException {
		Thread.sleep(1000);
		System.out.println("1");
	}
	public static synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:首先synchronized锁住的都是两个静态方法.并且锁住的类对象都是一样的.都是Number6.class,因此存在互斥.
  • 结果:1 2或者2 1

15.7第七锁

public class Lock7 {
	public static void main(String[] args) {
		 Number7 n1 = new Number7();
		 Number7 n2 = new Number7();
		 //线程1
		 new Thread() { public void run() {try {
			n1.a();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}}}.start();
		//线程2
		 new Thread() { public void run() {n2.b();}}.start();;
	}
}

class Number7{
	public static synchronized void a() throws InterruptedException {
		Thread.sleep(1000);
		System.out.println("1");
	}
	public   synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:首先synchronized分别锁住的是静态方法和成员方法.锁对象不同,一个是 类对象,一个是this(n2对象) 因此不存在互斥.
  • 结果:2 1

15.8第八锁

public class Lock8 {
	public static void main(String[] args) {
		 Number8 n1 = new Number8();
		 Number8 n2 = new Number8();
		 //线程1
		 new Thread() { public void run() {try {
			n1.a();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}}}.start();
		//线程2
		 new Thread() { public void run() {n2.b();}}.start();;
	}
}

class Number8{
	public static synchronized void a() throws InterruptedException {
		Thread.sleep(1000);
		System.out.println("1");
	}
	public  static synchronized void b() {
		System.out.println("2");
	}
}
  • 分析:synchronized锁住的是两个静态方法,这两个静态方法都输出Number8.class的对象.因此是相同的类变量.因此存在互斥
  • 结果:1 2或者2 1

16.变量的线程安全分析

16.1成员变量和静态变量是否线程安全?

  • 如果它们没有被共享,则是线程安全的.
  • 如果他们被共享了.根据它们的状态能否被改变又分为两种情况.
  •     如果只有读操作,则线程安全.(因为读操作不影响,内存中的值.)....但是会出现脏读
  •     如果有读写操作.,则这段代码的临界区,需要考虑线程安全了.

16.2局部变量是否线程安全?

  • 局部变量是线程安全的.
  • 但局部变量引用的对象则未必,
  • 如果该对象没有逃离方法的作用访问,它是线程安全的.
  • 如果该对象逃离方法的作用范围,需要考虑线程安全.

16.2.1局部变量线程安全分析.

     public static void test1() {
		int i = 10;
		i++;
	}
  • 每个线程在调用test1()时,局部变量i,会在每个线程的桢栈内存中会被创建多份,因此不存在共享
  • 局部变量实现线程私有的.(不共享就安全)

16.2.1局部变量引用的安全分析

public class TestThreadSafe {
	
	static final int THREAD_NUMBER=2;
	static final int LOOP_NUMBER=200;
	
	public static void main(String[] args) {
		ThreadUnsafe test = new ThreadUnsafe();
		for(int i=0;i<THREAD_NUMBER;i++) {
			new Thread() {
				public void run() {
					test.method1(LOOP_NUMBER);
				}
			}.start();
		}
	}
}

class ThreadUnsafe{
	ArrayList<String> list = new ArrayList<>();
	public void method1(int loopNumber) {
		for(int i = 0 ; i <loopNumber ;i++) {
			method2();
			method3();
		}
	}
	private void method2() {list.add("1");}
	private void method3() {list.remove(0);}
}
  • 有时候会出现:Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: -1
  •     at java.util.ArrayList.add(ArrayList.java:459)
  • 分析:无论在哪一个线程中,method2引用的都是同一个对象中的list成员变量.(共享了)

 

将list修改为局部变量.代码如下

class ThreadSafe{
	public final void method1(int loopNumber) {
		ArrayList<String> list = new ArrayList<>();
		for(int i =0;i<loopNumber;i++) {
			method2(list);
			method3(list);
		}
	}
	public void method2(ArrayList<String> list) {list.add("1");}
	public void method3(ArrayList<String> list) {list.remove(0);}
}
  • 分析:list是局部变量,每个线程调用时会创建其不同实例,没有共享.
  • 而method2的参数是从method1中传递过来的,与method1中引用同一个对象,
  • method3的参数分析与method2相同

方法访问修饰符带来的思考,如果把method2和method3的方法修改为public会不会出现线程安全问题?

  • 情况1:有其它线程调用method1和method2
    • method1,2有可能被其他线程调用,,比如线程1调用method1,线程2调用method2,因为线程1中的list是局部变量,私有的,线程2调用method2需要传入一个list,那么就是另外一个不同于method1中的list了.因此不存在线程安全问题.(各玩各的没事)
  • 情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖 method2或method3方法
    • 子类复写了method3并在其中又开启了一个线程,这就有问题了,这等于是两个线程玩同一个list.有可能会出事的.
class ThreadSafeSubClass extends ThreadSafe{
	@Override
	public void method3(ArrayList<String> list){
		new Thread() {
			public void run() {
				list.remove(0);	
			}
		}.start();
	}
}
  • 因此我们得出:方法的访问修饰符是有意义的,private,final使得子类不能影响父类的行为.在一定程度上加强了线程安全性.
  • 从这个例子中可以看出private  和final 提供的[安全]的意义所在,细品开闭原则中的[闭];

17.常见的线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.until.concurrent包下的类

17.1常见类的组合调用

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法,是线程安全的.也可以理解为:

  • 它们的每个方法是原子的.
  • 但注意它们多个方法的组合不是原子的.见后分析.
  • public static void main(String[] args) throws InterruptedException {
    		Hashtable<String,String> table = new Hashtable<String, String>();
    		
    		//线程1
    		Thread t1 = new Thread() {
    			public void run() {
    				if(table.get("key") == null) {
    					table.put("key", "t1");
    				}
    			}
    		};
    		//线程2
    		Thread t2 = new Thread() {
    			public void run() {
    				if(table.get("key") == null) {
    					table.put("key", "t2");
    				}
    			}
    		};
    		t1.start();
    		t2.start();
    		t1.join();
    		t2.join();
    		System.out.println(table.get("key"));
    	}

    分析:

17.2常见类-不可变.

String,Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的.

内部状态不可变?那String.class里面有substring方法,replace方法. 

分析:查看了String中substring方法,发现它并没有改变当前字符串,而是新建了一个新的String对象,在原有字符串的基础之上进行复制,截取的.

 public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        //在原有字符串上进行复制.截取的
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

replace也是一样的机制.

实例分析:

//是否线程安全?
	Map<String,Object> map = new HashMap<>();//不安全
	String s1 = "...";
	final String s2 = "...";
	Date d1 = new Date();
	final Date d2 = new Date();
	public void doGet(HttpServletRequest req,HttpServletResponse res) {
		//使用上述变量
	}
  • Map不是线程安全的,Hashtable是线程安全的.
  • String是线程安全的.Date不是线程安全的.
  • final Date d2只能确保.d2的引用不会发生改变,但是日期内部的属性是可以发生修改的.(存在读写操作);

例2:

public abstract class Test19 {
	
	public void bar() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		foo(sdf);
	}
	
	public abstract  void foo(SimpleDateFormat sdf);
	
	public static void main(String[] args) {
		new Test19().bar();
	}
}
  • 分析:因为foo的行为不确定,可能导致不安全发生,这种情况被称为外星方法.
  •  在复写父类foo中开启另一个线程,操作
  • String为什么是final的.
  • 这是为了避免子类继承String,因为有些子类覆盖父类的方法后,可能会出现线程不安全的情况,

17.3购票的例子。

public class Test1 {
	public static void main(String[] args) throws InterruptedException {

		TicketWindow window = new TicketWindow(10000);

		//卖出的票数统计
		List<Integer> amountList = new Vector<>();
		//所有线程的集合
		List<Thread> threadList = new ArrayList<>();
		
		// 开始卖票
		for (int i = 0; i < 1000; i++) {
			Thread thread = new Thread() {
				public void run() {
					int sellNumber=window.sell(getRandom());
					amountList.add(sellNumber);
				}// run-end
			};
			threadList.add(thread);
			thread.start();
		} // for-end
		
		//等待所有线程执行完毕。
		for(Thread thread:threadList) 
			thread.join();
		
		//卖出去的票数
		System.out.println("余票:"+window.getCount());
		System.out.println("卖出去的票数:"+amountList.stream().mapToInt(i->i).sum());
	}// main-end

	// Random为线程安全的
	static Random random = new Random();

	public static int getRandom() {
		return random.nextInt(5) + 1;
	}
}    

class TicketWindow {

	private int count;
	
	//构造器
	public TicketWindow(int count) {
		this.count=count;
	}
	
	//获得当前票数
	public int getCount() {
		return this.count;
	}
	
	//售票
	public int sell(int count) {
		if(this.count>=count) {
			this.count-=count;
			return count;
		}
		//不够买或者卖完了.
		else  
			return 0;
	}
}

 threadList是属于mian线程的,不会被多个线程调用到,因此是线程安全的.

两个不一样的对象调用的组合,如果单个都是线程安全的,组合到一起也是线程安全的.

对照hashtable的put,get.因为hashtable的调用对象都是同一个对象,尽管,它的put,get都是线程安全的,但是组合起来使用会出现不安全的情况.

17.4转账问题

public class ExerciseaTransfer {

	static Random random = new Random();
	
	public static int randomAmount() {return random.nextInt(100)+1;}
	
	
	public static void main(String[] args) throws InterruptedException {
		Account a = new Account(1000);
		Account b=new Account(1000);
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<1000;i++) {
					a.transfer(b,randomAmount());
				}
			}
		};
		
		Thread t2 =new Thread() {
			public void run() {
				for(int i=0;i<1000;i++) {
					b.transfer(a, randomAmount());
				}
			}
		};
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("A账户的钱:"+a.getMoney());
		System.out.println("B账户的钱:"+b.getMoney());
	}
}

//用户类
public class Account {

		private int money;
		
		public Account(int money) {	this.money=money;}
		
		public int getMoney() {return this.money;}
		
		public void setMoney(int money) {this.money=money;}
		
		//转账
		public   void transfer(Account targer,int amount) {
			synchronized(Account.class) {
				if(this.money>=amount) {
					this.setMoney(this.money-amount);
					targer.setMoney(targer.getMoney()+amount);
				}
			}
		}
}
  • 分析:线程不安全一定会出现在transfer方法内.(因为里面做了读写操作),
  • 解决办法:1.将两个类都添加上锁.(不可行:因为这样有可能会导致死锁
  • 2.提取通性,对Account.class进行加锁.(但是效率不高.只能对两个账户进行操作,其他的用户转账只能等待)

 18.Monitor概念

18.1Java对象头-  也可以理解为计网中的数据报的首部信息

以32位虚拟机为例

  • 普通对象:   Object Header(64bit)=Mark Work(32bit)+Klass Word(32bit)
  • 数组对象: Object Header(96bit) = Mark Work(32bit)+Klass Word(32bit) + Array Length(32bit)
  • Mark Word 主要是用来
  • Klass Word只要是用来找到该对象的类对象的. 

18.2Monitor(锁或者叫做管程)

每个Java对象都可以关联一个Monitor对象.如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置为指向Monitor对象的指针.

Monitor结构如下:

  • 刚开始Monitor中的Owner为null;
  • 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner设置为Thread-2,Monitor中有且只能有一个Owner
  • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList中 阻塞.
  • Thread-2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的.(即先来不一定先得)
  • 图中WaitSet中的Thread-0,Thread-1是之前获得过锁的,但条件不满足进入WAITING状态的线程.
  • 注意
    • synchronized必须是进入同一个对象的monitor中才会上述保护效果.
    • 不加synchronized的对象不会关联监视器,自然不会遵从上述规则.

19.Synchronized原理の进阶 

19.1轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的.(即没有出现竞争),那么可以使用轻量级锁来进行优化.

轻量级锁对使用者是透明的,即语法仍为:synchronized(...){...}

假设有两个方法同步块,利用同一个对象加锁,

        static final Object obj= new Object();
        public static void method1(){synchronized(obj){
            ...               
            method2();
        }}
        public static void method2(){synchronized(obj){
            ....
        }}
  • 创建锁记录(Lock Record)对象,每个线程的桢栈中都会包含一个锁记录的结构体,内部可以存储锁定对象的Mark Word.
  • 让锁记录中Object refrenence指向锁对象,并尝试用cas替换Object 的Mark Word ,将Mark Word的值存入锁记录中.
  • 如果cas替换成功,对象头中存贮了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
  • 如果cas失败有两种情况:
    • 如果是其他线程已经持有了该对象的轻量级锁,这时表明有竞争.进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添一条Lock Record作为重入的计数.只不过重入的所记录的MarkWord为null.
    • 当退出synchronized代码块(解锁时)如果有取值为null的记录,表示有重入,这时重置锁记录,表示重入计数减一;
    • 当退出synchronized代码块时的锁记录不为空,这时使用cas将Mark Word的值回复给对象头
      • 成功,解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,那就进入了重量级锁的解锁流程.
        • 轻量级锁没有阻塞功能,因此转入重量级锁中就包含了阻塞功能(EntryList)这也可能是出于这个原因
  • 这时Thread-1线程加轻量级锁失败了,接着进入了锁膨胀状态.
    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址.
    • 然后自己进入Monitor的EntryList中阻塞,进行等待.
  • 当Thread-0退出同步块解锁时,使用cas将MarkWord的值回复给对象头,结果失败了.因为此时的锁已经升级为重量级锁,会进入重量级锁的解锁流程.即按照Monitor地址找到Monitor对象,设置Ower为空,并且唤醒EntryList中的阻塞的线程.

19.2自旋优化

重量级锁在竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,(即这时持锁线程已经退出了同步块释放了锁.),这时可以避免阻塞.但是如果自旋重试仍然没有获得资源也会进行阻塞.

通俗的讲:别人再用,你也想用,但是一个时刻只能一个人用,所以你会在等的过程中过去看看用完没.如果刚好用完了,你无缝衔接.节约了整体的时间.如果过去看的次数到达一定的闸值,等的人就不过去看了.乖乖的在原地等(阻塞).

在Java6之后自旋锁变成了自适应的,比如对象最近一次自旋成功,那么它就会认为目前的条件适合自旋,它就会多自旋几次.反之,就会少自旋甚至不自旋.

此外,自旋的话或占用Cpu,单核条件下自旋等于浪费时间.多核条件下才能发挥其优势.

java7之后,不能控制自旋的开关了.

19.3偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作.

Java6中引入了偏向锁来进一步优化:只有第一次使用cas将线程ID设置到MarkdWord中,之后发现这个线程ID是自己就表示没有竞争.不用重新Cas操作.以后只要不发生竞争.这个对象就归该线程所有.

轻量级锁:

偏向锁:

对比:偏向锁省去了Cas操作,只需要一个if判断即可.

当一个对象被创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,MardWord值的最后三位:101,其中第一个1代表启用偏向锁,01表示无锁状态.并且此时的,thread,epoch,age都为0
  • 偏向锁默认是延迟的,不会在程序启动时立刻生效,如果想立刻生效,可以添加JVM参数, -XX:BiasedLockingStartupDelay=0来去掉时延.
  • 如果没有开启偏向锁,那么对象创建后,markword最后3位为:001,hashcode,age都为0,
  • 处于偏向锁的对象解锁之后,线程id仍存储于对象头中.
  • 禁用偏向锁的JVM参数: -XX :-UseBiasedLocking禁用偏向锁
  • 偏向锁是优化后的轻量级锁,可以减少一次cas操作,提高性能.
  • 调用hashcode会使得偏向锁禁用,变成不可偏向的.
  • 优先级:有偏向锁会用偏向锁,如果有其他线程使用了锁对象会变为轻量级锁,如果发生竞争会升级为重量级锁.

19.3.1撤销偏向锁--调用对象的HashCode

调用了对象的HashCode,但偏向锁的对象MarkWord中存储的是线程ID,如果调用对象的Hashcode会导致偏向锁的线程ID被覆盖,即偏向锁被撤销了.

  • 轻量级锁会在锁记录(Lock Record)中记录hashCode
  • 重量级锁会在Monitor中记录hashCode

19.3.2撤销偏向锁---其他线程使用对象

当有其他线程要使用偏向锁对象时,会将偏向锁升级为轻量级锁,

偏性锁:本来我这个是个t1线程专用的,这又来了一个,使得我来回偏向比较累(耗费Cpu)..那就换锁(轻量级锁)呗.

19.3.3撤销偏向锁---调用wait/notify方法.

因为wait和notify方法是唤醒阻塞线程用的,那就需要EntryList.它们两个是重量级锁才拥有的特性.

19.4批量重偏向

如果对象被多个线程访问,但是没有发生竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向后会重置对象的Thread-ID

当撤销偏向锁的次数超过20次后,jvm会觉得我是不是偏向错了,于是会再给这些对象加锁时重新偏向至加锁线程.

19.5批量撤销

当撤销偏向锁闸值超过40次后,jvm会觉得自己确实偏向错了,于是将整个类所有的对象设为不可偏向.新建的对象也不可偏向.

20.锁消除

static int x=0;
public void a(){x++;}

public void b(){
    Object o = new Object();
    synchronized(o){
        x++;
    }
}

逃逸分析,b方法中的synchronized根本不会逃离方法体中,因此不会被共享.所以这个同步锁也就没有必要了,Jvm对其进行了优化在实际执行过程中去除了锁..因此a,b方法的耗费时间几乎一致.

21.wait/notify

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态.
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片.
  • BLOCKED线程会在Owner线程释放锁时唤醒.
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争锁.

21.1 API的介绍

  • obj.wait()让进入object监视器的线程到WaitSet中等待(会释放锁,离开Owner)
  • obj.notify(),在object上正在WaitSet等待的线程中挑一个唤醒(随机).
  • obj.notifyAll(),唤醒所有在object上正在WaitSet中等待的线程.
  • 它们都是线程之间进行协作的手段,都属于Object对象的方法.必须获得此对象的锁,才能调用这几个方法.

问题:wait和sleep的区别?

wait会释放掉Owner(锁),而sleep不会释放Owner(锁).

  • 带参数的wait(int n)方法. n的作用就是等n毫秒后自动唤醒.也可以被notify/notifyAll提前唤醒.
  • wait();实际上就是在内部又调用了一次wait(0); //0代表一直等,直到被notify/notifyAll唤醒.

22.wait/notify的正确姿势

sleep(long n)和wait(long n)的区别

  1. sleep是Thread的静态方法,而wait是Object的方法.(从API来源の异)
  2. sleep不需要强制和synchronized配合使用,但wait必需要和synchronized一起用.因为wait只有Owner才能调用.必需先要持有锁.
  3. sleep在睡眠同时,不会释放对象锁.但wait在等待时会释放对象锁.(失去Owner进入WaitSet中等待唤醒.)
  4. 它们的状态都是TIMED_WATING (相同之处)
synchronized(lock){
    while(条件不成立){
        lock.wait;
    }
    //干活
}

//另一个线程
synchronized(lock){
    lock.notifyAll();//全唤醒
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值