1.volatile与Java内存模型

在Java中Java的内存模型是围绕着原子性,有序性和可见性展开的。

当在Java中用volatile去声明一个变量时,相当于告诉虚拟机这个变量时容易被程序和线程修改,所以为了确保这个变量被修改后应用程序范围内所有的线程都能看到这个变量的改动虚拟机必须采用一些特殊的手段来保证这个变量的可见性。

但是,需要注意的是虽然volatile对于操作的原子性有所帮助但是它不能保证复合操作的原子性。

举个例子:

package thread;

public class VolatileDemo {
	static volatile int i=0;
	public static class Test implements Runnable{

		@Override
		public void run() {
			for(int k=0;k<1000;k++)
				i++;
		}
		
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread[] threads=new Thread[10];
		for (int i = 0; i < 10; i++) {
			threads[i]=new Thread(new Test());
			threads[i].start();
		}
		for (int i = 0; i < 10; i++) {
			threads[i].join();
		}
		
		System.out.println(i);
	}

}

可以看到输出并不是10000,这是因为volatile不能保证原子性。

但是它能保证数据的可见性和有序性。

package thread;

public class VolatileDemo {
	private static volatile boolean ready;
	private static volatile int number=0;
	

	
	
	public static class Test2 implements Runnable{

		@Override
		public void run() {
			while(!ready);
			System.out.println(number);
		}
		
	}
	
	public static void main(String[] args) throws InterruptedException {

		
		new Thread(new Test2()).start();
		Thread.sleep(1000);
		number=22;
		ready=true;
		Thread.sleep(5000);
	}

}

打印为22,上面的代码只有在ready为true才会打印number值,但是如果你将ready的volatile去掉你会发现程序无法打印值,这是因为可见性的问题。

2.线程组

简单介绍下线程组的使用:

package thread;

public class ThreadGroupName implements Runnable{

	@Override
	public void run() {
		String groupAndName=Thread.currentThread().getThreadGroup().getName()+"-"
				+Thread.currentThread().getName();
		while(true) {
			System.out.println("Thread name:"+groupAndName);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}		
		
	}
	
	public static void main(String[] args) {
		ThreadGroup tg=new ThreadGroup("线程组");
		Thread t1=new Thread(tg,new ThreadGroupName(),"t1");
		Thread t2=new Thread(tg,new ThreadGroupName(),"t2");
		t1.start();
		t2.start();
		System.out.println(tg.activeCount());
		tg.list();
		
	}

}

ThreadGroup tg=new ThreadGroup("线程组");通过这行代码我新建了一个名为“线程组”的线程,并将t1和t2加入其中。而activeCount()可以获取活动线程的总数,因为线程是动态的所以只是一个估计值。list()方法可以打印这个线程组的所有线程信息。线程组也有stop()方法,它会停止这个线程组里所有的线程。但是会遇到Thread.stop()相同的问题,所以慎重使用。

3.守护线程(Deamon)

守护线程,如:垃圾回收线程,JIT线程都是守护线程。相对应的就是用户线程,用户线程会完成这个程序要完成的业务操作,如果用户线程全部结束也就意味这个程序实际上已经无事可做。因此可以说当一个Java应用中只有守护线程时,Java虚拟机会自然退出。

设置一个守护线程只需要setDaemon()设为true。需要注意的是守护线程的设置需要在start()之前设置。尽管程序能继续运行,但是它已经被当做是用户线程。

package thread;

public class DaemonDemo {
	public static class Daemon implements Runnable{

		@Override
		public void run() {
			while(true) {
				System.out.println("110");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread t=new Thread(new Daemon());
		t.setDaemon(true);
		t.start();
		
		Thread.sleep(2000);
	}


}

可以看到如果将设置守护线程的代码放到start()后面,程序会一直运行,因为此时没有守护线程,而t是用户线程所以会一直打印。

4.线程优先级

线程优先级高的线程在竞争资源时会更有优势,但是这仅仅是个概率问题,并且在多线程实战中也有讲到不推荐修改线程优先级因为可能会出现饥饿的情况。线程的优先调度与底层操作系统有关,并且无法精确控制。

java中使用1~10表示线程优先级

package thread;

public class PriorityDemo {
	public static class Daemon implements Runnable {

		static volatile int count = 0;
		static volatile int t1Num = 0;
		static volatile int t2Num = 0;

		@Override
		public void run() {
			while (true) {
				synchronized (Daemon.class) {
					if (count == 10000000) {
						System.out.println(t1Num);
						System.out.println(t2Num);
						break;
					}
					count++;
					if (Thread.currentThread().getName().equals("t1")) {
						t1Num++;
						System.out.println(Thread.currentThread().getName() + count);
					}
					else
						t2Num++;
				}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Daemon(), "t1");
		Thread t2 = new Thread(new Daemon(), "t2");
		t.setPriority(10);
		t2.setPriority(1);
		t.start();
		t2.start();
		Thread.sleep(2000);
	
	}
}

可以看到……t1承包了整个“鱼塘”……

如果将设置优先级的代码进行注释:

感觉好多了……

5.线程安全与synchronized

程序并行化是为了获取更高的执行效率,这个的首要前提是正确性。

package thread;

public class PriorityDemo {
	static volatile int count = 0;

	public static class Daemon implements Runnable {

		@Override
		public void run() {
//			synchronized (Daemon.class) {
				for(int i=0;i<1000000000;i++)
					count++;
				
//			}
		}



	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Daemon(), "t1");
		Thread t2 = new Thread(new Daemon(), "t2");
		t.start();
		t2.start();
		t.join();
		t2.join();
		System.out.println(count);
	}
}

运行结果如图所示,这是因为多线程的写入冲突,当两个线程同时读取count的值为0时,并且各自计算得到count=1虽然count++执行两次但是它实际上只增加了一次。

如果要从根本上解决这个问题,我们要保证多个线程对count进行操作时要同步处理。也就是说A线程在写入是B线程既不能读也不能写。因为如果此时A线程在写入,那么B线程读取的就是一个过期数据,在这里我们通过synchronized来实现。

这时候把上面代码的synchronized的注释去除则:

synchronized关键字的作用是实现线程之间的同步。通过对需要同步的代码进行加锁,使每一次只有一个线程进入同步块,从而保证线程的安全性。

synchronized的用法:

1.直接加锁对象:对给定的对象加锁,进入同步代码前要获得给定对象的锁。

2.直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

3.直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

package thread;

public class AccountingSync implements Runnable{

	static AccountingSync accountingSync=new AccountingSync();
	static int i=0;
	@Override
	public void run() {
		for(int k=0;k<10000000;k++) {
			synchronized (this) {
				i++;
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		Thread t1=new Thread(accountingSync);
		Thread t2=new Thread(accountingSync);
//		Thread t1=new Thread(new AccountingSync());
//		Thread t2=new Thread(new AccountingSync());
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
		
	}
}

这段代码会输出20000000,但是如果你将hread t1=new Thread(new AccountingSync());Thread t2=new Thread(new AccountingSync());的注释去除你会发现i的值并不是20000000这是因为这个两个线程指向了不同的Runnable实例,这会使得同步不起作用(因为持有的不是同一个锁),解决方案是将this换成AccountingSync.class。

除了线程同步,确保线程安全,synchronized还确保了线程的可见性和有序性。(总的来说被synchronized限制的多个线程是串行执行的。)

附:

package thread;

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo{
	static Map<Integer,String> map=new HashMap<Integer,String>();
	
	public static class AddThread implements Runnable{

		int start=0;
		public AddThread(int i)
		{
			this.start=i;
		}	
		@Override
		public void run() {
			for(int i=start;i<100000000;i++) {
				map.put(i, Integer.toBinaryString(i));
			}
		}
		
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread t1=new Thread(new HashMapDemo.AddThread(0));
		Thread t2=new Thread(new HashMapDemo.AddThread(0));
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(map.size());
	}
	
}

这段代码会使CPU达到100%……

 

需要注意的是在进行加锁的时候不要使用Integer,String等类做synchronized()中的锁。因为Integer等在Java中属于不变对象,一旦创建就不会修改,如果你对其进行了修改对返回一个新的引用。这点需要注意。

参考《实战Java高并发程序设计》

点击阅读全文
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐