目录

并发

并发带来的问题

同步(synchronized)

synchronized方法实现代码:

synchronized代码块实现代码


并发

原文地址: http://www.cnblogs.com/dolphin0520/p/3910667.html

进程出现之前,每次都是一个程序在计算机里面运行,也就说内存中始终只有一个程序的运行数据。而如果想要任务A执行I/O操作的时候,让任务B去执行,必然内存中要装入多个程序,那么如何处理呢?多个程序使用的数据如何进行辨别呢?并且当一个程序运行暂停后,后面如何恢复到它之前执行的状态呢?

这个时候人们就发明了进程,用进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂时时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。

这就是 并发:能够让操作系统从宏观上看起来同一个时间段有多个任务在执行。换句话说,进程让操作系统的并发成为了可能。

进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。


并发带来的问题

原文地址:http://www.cnblogs.com/sheeva/p/6366782.html

由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:

如果多个线程要同时访问某个资源,怎么处理?比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据。

其实多线程根本的问题只有一个:线程间变量的共享  这里的变量,指的就是类变量和实例变量,后续的一切,都是为了解决类变量和实例变量共享的安全问题:

java里的变量可以分3类:

  1. 类变量(类里面static修饰的变量)
  2. 实例变量(类里面的普通变量)
  3. 局部变量(方法里声明的变量)

下图是jvm的内存区域划分图:

根据各个区域的定义,我们可以知道:

  1. 类变量 保存在“方法区”
  2. 实例变量 保存在“堆”
  3. 局部变量 保存在 “虚拟机栈”

“方法区”和“堆”都属于线程共享数据区,“虚拟机栈”属于线程私有数据区。

因此,局部变量是不能多个线程共享的,而类变量和实例变量是可以多个线程共享的。事实上,在java中,多线程间进行通信的唯一途径就是通过类变量和实例变量。

也就是说,如果一段多线程程序中如果没有类变量和实例变量,那么这段多线程程序就一定是线程安全的。以Web开发的Servlet为例,一般我们开发的时候,自己的类继承HttpServlet之后,重写doPost()、doGet()处理请求,不管我们在这两个方法里写什么代码,只要没有操作类变量或实例变量,最后写出来的代码就是线程安全的。

为了解决并发带来的变量共享的问题,在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。


synchronized(同步)

在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。

在共享数据里保存一个锁,当没有线程访问时,锁是空的,当有第一个线程访问时,在锁里保存这个线程的标识并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,要等待锁释放

先了解两个大体的概念:

  • MarkWord:java中的每个对象在存储的时候,都有统一的数据结构。每个对象都包含一个对象头,称为MarkWord,里面会保存关于这个对象的加锁信息
  • Lock Record : 即锁记录,每个线程在执行的时候,会有自己的虚拟机栈,单个方法的调用相当于虚拟机栈里的一个栈帧,而Lock Record 就位于栈帧上,是用来保存关于这个线程的加锁信息。

jvm中的三种锁都是以上述思想为基础的,只是实现的“重量级”不同,jvm中有以下三种锁(由上到下越来越“重量级”):

  1. 偏向锁
  2. 轻量级锁
  3. 重量级锁

其中重量级锁是最初的锁机制,偏向锁和轻量级锁是在jdk1.6加入的,可以选择打开或关闭。如果把偏向锁和轻量级锁都打开,那么在java代码中使用synchronized关键字的时候,jvm底层会尝试先使用偏向锁,如果偏向锁不可用,则转换为轻量级锁,如果轻量级锁不可用,则转换为重量级锁。

三种锁虽然实现细节不同,但是都是使用MarkWord保存锁的:

  1. 偏向锁    是在MarkWord里 保存线程id;
  2. 轻量级锁是在MarkWord里 保存指向拥有锁的线程栈中锁记录的指针;
  3. 重量级锁是在MarkWord里 保存指向互斥量的指针;

偏向锁最快但是只能用于从始至终只有一个线程获得锁,

轻量级锁较快但是只能用于线程串行获得锁,

重量级锁最慢但是可以用于线程并发获得锁,

先用最快的偏向锁,每次假设不成立就升级一个重量。


synchronized方法的代码:

public class Test {
 
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
         
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
    }  
}

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
     
    public void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
}
  • 运行结果:

Thread-0在插入数据0
Thread-1在插入数据0
Thread-0在插入数据1
Thread-1在插入数据1
Thread-0在插入数据2
Thread-1在插入数据2
Thread-0在插入数据3
Thread-1在插入数据3
Thread-0在插入数据4
Thread-1在插入数据4

 加入synchronized 关键字后:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    // 加了synchronized 的方法
    public synchronized void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
}
  • 运行结果:

Thread-0在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-0在插入数据3
Thread-0在插入数据4
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
Thread-1在插入数据3
Thread-1在插入数据4


synchronized代码块的代码

也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public void insert(Thread thread){
        synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

注意:

  1. 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,该对象的锁里也保存了第一个线程的标识,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
  2. 当一个线程正在访问一个对象的synchronized方法,那么 其他线程能访问该对象的非synchronized方法。因为访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的。
  3. 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁
  4. 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
  5. 当线程 请求由自己持有的对象锁 时,如果该锁是重入锁,请求就会成功,否则阻塞:在一个Synchronized修饰的 方法或代码块 的内部 调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
  6. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
  7. 死锁 是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是,一旦程序发生死锁,程序将死掉。
  8. synchronized关键字是不能继承的 。也就是说,基类的方法 synchronized f(){  } 在继承类中并不自动是synchronized f( ){  },而是变成了f( ){  }。 继承类需要你显式的指定它的某个方法为synchronized方法;
  9. 另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。但是,如果一个线程执行一个对象的 非static synchronized方法,另外一个线程需要执行这个对象所属类的 static synchronized方法,此时不会发生互斥现象,因为访问 static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。