Java多线程学习笔记

Android异步加载问题确实废了我不少时间,很多问题出现又很多问题被解决。自己算的上是有一点心得的了。从简单的HttpURLConnection到异步、再到ImageLoader、再到Volley。虽然现在还不是很熟练那种。在没有用到框架之前是自己一步一步写的,然后考虑到使用多线程优化问题,那时候自己对多线程了解的不多,看了下牛人的博客,觉得写的蛮好的,自己概括整理当成学习笔记分享出来,同时也供以后查阅使用。


转载请注明出处:http://blog.csdn.net/yang_ge_98/article/details/47440375


下面就具体的概括


3、介绍顺序大致为:
1)同步介绍、锁的概念
2)synchronized实现同步介绍
3)Lock同步介绍
4)协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll)
5)ThreadLocal如何解决并发安全性
6)Java线程安全的几种方法对比


一、同步介绍和synchronized讲解


1、同步在多线程访问同一个资源的时候起作用。没有同步的时候,多线程运行是没有规律、争先恐后的。要保证他们共同访问统一资源时不会出错就设置同步


2、同步有同步方法或者同步代码段(有同步的代码块),他们分别是下面的样子:

1)同步方法:
public synchronized void function(){}
2)同步代码段:
public void run() {

	synchronized (lock) {
		//这里是同步的代码块。。。
	}
}

3、同步一般跟锁紧密相连。


1)用一个很通俗的例子来理解下这个锁的概念

车站里面人很多,如果这个车站只有一个厕所,那就会出现很多人要用这个厕所的情形,这就类似多线程访问同一资源。厕所一次只能一个人使用,当某个人(相当于一个线程)进去厕所的时候门被锁上,其他人无法进去,这有等这个人打开门出来以后才可以进去使用(当一个线程正在执行某个同步的方法或者代码块,就得到了一个锁,只有当该线程释放了这个锁之后,其他的线程才能执行这个方法。这些其他线程的执行也是一个得到和释放锁的过程)。


2)这样子应该就清楚这个锁对于多线程访问同一资源来说是多么重要的了。那么这个锁是怎么体现的?

Java虚拟机通过“相关联的监视器”来保护类或者对象,为每一个对象和类都关联一个锁。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。也就是说每个类或者类的每个对象都可以是一把锁,同步方法或者同步代码块中都会有一把锁,这把锁要么是一个类(xxx.class)、要么是一个对象。

3)这样说就明白了这个锁的情况了。

每个类和类的每个对象都是一把锁,定义一个同步方法或者代码块,它会有一个锁,这个锁可以是一个类或者是一个对象。如果同步中指定了锁,则一次就只能让一个线程执行。如果多个同步中指定了相同的锁,当一个线程在其中一个同步方法或者代码块中得到了这个锁并且没有释放时,其他所有线程都不能执行这里面的所有同步方法或者代码块,因为这个锁没有释放,他们拿不到(比如方法1、代码块1的锁都是相同,线程a执行了方法1之后执行代码块1,在线程a没有离开代码块1前其他线程不但不能执行代码块1,甚至不能执行方法1。因为对他们来说方法1的锁他们没有获得)。

4)刚刚释放了该锁的线程会优先获得该锁,这个只在循环执行同步或者多个同步相连并且这些同步的锁相同有效。


4、同步中锁的指定


1)同步可以是同步方法或者代码块,前面也分别写出了他们的样子。我们可以看到同步方法只是在方法定义上加上了 synchronized 修饰符,同步代码块则在花括号前用 synchronized 修饰并用小括号指定具体的锁。


2)同步方法的指定是这样的:静态方法的锁是类本身(也就是xxx.class对象),非静态方法的锁是某个对象。静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法(不用担心是否单例)。看下面的一个例子:

public class ThreadTest extends Thread {
	private int threadNo;
	
	public ThreadTest(int threadNo) {
		this.threadNo = threadNo;
	}


	public static void main(String[] args) throws Exception {
		for (int i = 1; i < 10; i++) {
			new ThreadTest(i).start();
			Thread.sleep(1);
		}
	}
	
	@Override
	public synchronized void run() {
		for (int i = 1; i < 10000; i++) {
			System.out.println("No." + threadNo + ":" + i);
		}
	}
}


run方法中作了同步处理,但是这个run方法不是静态方法,所以他指定的锁为这个方法所在的对象本身。因为每个线程就是一个ThreadTest对象,它的run方法的锁就是线程对象本身,所以发现输出的结果是乱七八糟的。

3)代码块指定锁就很方便了。对于下面的代码,将(lock)中的lock指定为某个对象或者类,这个锁就指定好了。
public void run() {

	synchronized (lock) {
		//这里是同步的代码块。。。
	}
}

5、synchronized获得锁和释放锁是自动的。开始执行同步时获得锁,执行完毕自定释放该锁。



二、java.util.concurrent.locks.Lock的使用(一)

1、synchronized与Lock的异同。
主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。


2、Lock是一个接口,ReentrantLock类实现了Lock类。注意在finally中释放

public class ThreadDemo implements Runnable {
    class Student {
        private int age = 0;

        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
    }
    Student student = new Student();
    int count = 0;
    
    //定义了两个重入锁
    ReentrantLock lock1 = new ReentrantLock(false);
    ReentrantLock lock2 = new ReentrantLock(false);

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        for (int i = 1; i <= 3; i++) {
            Thread t = new Thread(td, i + "");
            t.start();
        }
    }
    public void run() {
        accessStudent();
    }
    
    public void accessStudent() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        
        lock1.lock();//使用重入锁,同步开始
        System.out.println(currentThreadName + " got lock1@Step1!");
        try {
            count++;
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(currentThreadName + " first Reading count:" + count);
            
            lock1.unlock();//同步结束,释放重入锁
            System.out.println(currentThreadName + " release lock1@Step1!");
        }

        lock2.lock();//使用另外一个不同的重入锁(就是另一个锁)
        System.out.println(currentThreadName + " got lock2@Step2!");
        try {
            Random random = new Random();
            int age = random.nextInt(100);
            System.out.println("thread " + currentThreadName + " set age to:" + age);

            this.student.setAge(age);

            System.out.println("thread " + currentThreadName + " first  read age is:" + this.student.getAge());

            Thread.sleep(5000);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());
            lock2.unlock();//同步结束,释放重入锁
            System.out.println(currentThreadName + " release lock2@Step2!");
        }


    }
}

3、对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。
这说明两点问题:
1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码 人员自行掌握。
2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。
3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而 在finally中使用unlock方法。



二、java.util.concurrent.locks.Lock的使用(二)

1、首先,ReentrantLock有一个带布尔型参数的构造函数,在JDK官方文档中对它是这样描述的:
“此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock  方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。 ”


2、简单来讲:公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。 


3、但是任何事物都是有两面性的。
1).使用Lock,你必须手动的在finally块中释放锁。锁的获得和释放是不受JVM控制的。这要求编程人员更加细心。
2).当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。



三、java多线程协作(监视器就是锁)

1、Java监视器(锁)支持两种线程:互斥和协作。采用对象锁和重入锁来实现的互斥,采用wait、notify、notifyAll实现协作。
1)wait、notify、notifyAll都是Object的方法,而不是Thread的方法。所以调用一般用锁来调用这些方法。

2)当一个线程执行中遇到wait()方法,线程的执行被挂起,并释放锁。

3)notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法,并且遇到该方法后释放锁,但他本身不被挂起,仍然继续执行。两者的最大区别在于:


(1)notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

(2)notify只选择唤醒一个wait状态线程,并使它获得该对象上的锁,当第一个线程运行完后释放锁后如果没有再次使用notify,则即便该对象已空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。


2、在JVM中,有使用协作的监视器被称为等待并唤醒监视器。

1)在这种监视器中,一个已经持有该锁的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。

3、当程序为:

public class Texst extends Thread {
	public static void main(String[] args) {
		new S(){}.start();
		new S(){}.start();
		new S(){}.start();
		new S(){}.start();
		new S(){}.start();
		new S(){}.start();
	}
}


class S extends Thread{


	@Override
	public void run() {
		T.fun();
	}
}


class T {
	static int count = 5;
	static int age = 0;
	
	public static void fun() {
		synchronized (T.class) {
			if (T.count > 0) {
				T.count--;
				System.out.println(Thread.currentThread().getName() + "-----" + T.count);
				try {
					T.class.wait();
					System.out.println(Thread.currentThread().getName()+"原地复活了");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			T.class.notifyAll();
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			T.age++;
			System.out.println(Thread.currentThread().getName() + "--gfdgtdr---" + T.age);


		}
	}
}

打印结果为:
Thread-0-----4
Thread-2-----3
Thread-1-----2
Thread-3-----1
Thread-4-----0
Thread-5--gfdgtdr---1
Thread-4原地复活了
Thread-4--gfdgtdr---2
Thread-3原地复活了
Thread-3--gfdgtdr---3
Thread-1原地复活了
Thread-1--gfdgtdr---4
Thread-2原地复活了
Thread-2--gfdgtdr---5
Thread-0原地复活了
Thread-0--gfdgtdr---6


说明:
1)线程得到锁(监听器)执行同步方法遇到wait()后被原地挂起,等待notify()或者notifyAll()通知。得到通知并且重新获得锁的线程将会从原来挂起的位置继续执行。
2)如果有很多很多线程(超过六个),前面五个wait了,剩下几个挡在同步外面。好像执行notify默认是外面没有被wait并且没有进来同步方法的线程优先拿到锁。



4、用ReentrantLock的lock()和unlock()方法也可以同步,但是ReentrantLock不能跟wait或者notify方法一起使用。

会抛出java.lang.IllegalMonitorStateException异常的,不仅如此,你甚至还会看到线程死锁。原因就是当某个线程调用第三方对象的wait或者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息。但此时,程序流程如果没有用finally来处理unlock方法,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了,你用JConsole这种工具来检测JVM死锁,还检测不出来。


1)所以要么只使用ReentrantLock而不使用wait或者notify方法,因为ReentrantLock已经对这种互斥和协作进行了概括。所以,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用。


好了,我们现在明白:
1. 线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。
2. 线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。
3. 线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作。



四:java.lang.ThreadLocal解决线程并发的问题
1、前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。


2、当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。


ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、
将此线程局部变量的当前线程副本中的值设置为指定值、
移除此线程局部变量当前线程的值、
返回此线程局部变量的当前线程的“初始值”。



3、可见,要正确使用ThreadLocal,必须注意以下几点:


1). 总是对ThreadLocal中的initialValue()方法进行覆盖。

2). 当使用set()或get()方法时牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程!

3). 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。



运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。


现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。


当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。


把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新的思路。它为每个线程创建一个独立的资源副本,从而将多个线程中的数据隔离开来,避免了同步所产生的性能问题,是一种“以空间换时间”的解决方案。
但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。



以上都是借鉴这个牛人的观点,对于多线程同步问题的研究,这位大神讲的已经很周到很清晰了,如果看我的学习笔记不足以让你清楚的话,你可以到原博主的博客中学习。

http://www.blogjava.net/zhangwei217245/archive/2010/04/24/315080.html


你可能感兴趣的:(java)