“本文为传智大数据课程视频资料视频整理汇总”
- (一)多线程基础概念
- 什么是进程
- 什么是线程
- 什么是多线程
- 多线程运行原理
- 实现线程的两种方式
- 继承Thread
- 实现Runnable接口
- (二)Java同步关键字
- synchronize
- volatile
- lock
- Lock和synchronized的选择
(一)多线程基础概念
什么是进程?
不同的应用程序运行的过程中都需要在内存中分配自己独立的运行空间,彼此之间不会相互的影响。我们把每个独立应用程序在内存的独立空间称为当前应用程序运行的一个进程。
进程是内存中的一段独立的空间,可以负责当前应用程序的运行。当前这个进程负责调度当前程序中的所有运行细节。
什么是线程?
在一个进程中,每个独立的功能都需要独立的去运行,这时又需要把当前这个进程划分成多个运行区域,每个独立的小区域(小单元)称为一个线程。
线程:它是位于进程中,负责当前进程中的某个具备独立运行资格的空间。
进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。一个进程中至少应该有一个线程。
如:启动的QQ聊天软件,需要和多个人进行聊天。这时QQ聊天的软件进程中有多个聊天会话的线程。
什么是多线程?
多线程:在一个进程中,我们同时开启多个线程,让多个线程同时去完成某些任务(功能)。真正可以完成程序运行和功能的实现靠的是进程中的线程。
(比如后台服务系统,就可以用多个线程同时响应多个客户的请求)
多线程的目的:提高程序的运行效率。
多线程的运行原理
cpu在线程中做时间片的切换。
其实真正电脑中的程序的运行不是同时在运行的。CPU负责程序的运行,而CPU在运行程序的过程中某个时刻点上,它其实只能运行一个程序。而不是多个程序。而CPU它可以在多个程序之间进行高速的切换。而切换频率和速度太快,导致人的肉眼看不到。
每个程序就是进程, 而每个进程中会有多个线程,而CPU是在这些线程之间进行切换。我们必须知道,多线程可以提高程序的运行效率,但不能无限制的开线程。
实现线程的两种方式
继承Thread
代码
1 public class ThreadExtends extends Thread { 2 3 /** 4 * 实现多线程的方法一: 5 * 1、继承Thread 6 * 2、重写run方法 7 */ 8 @Override 9 public void run() { 10 String tname = Thread.currentThread().getName(); 11 System.out.println(tname + "线程运行中...."); 12 } 13 14 /** 15 * 使用线程, 16 * 1、 17 * 2、调用start() 18 * 19 * @param args 20 */ 21 public static void main(String[] args) { 22 ThreadExtends te = new ThreadExtends(); 23 te.run();// 如果是调用thread的run方法,则只是一个普通的方法调用,不会开启新的线程 24 te.start(); 25 26 } 27 }
运行结果
实现Runnable接口
代码
1 public class ThreadRunnable implements Runnable{ 2 3 int x ; 4 5 public ThreadRunnable(int x){ 6 this.x = x; 7 } 8 9 /** 10 * 实现多线程的方法二: 11 * 1、实现Runnable接口 12 * 2、实现run方法 13 */ 14 @Override 15 public void run() { 16 String tname = Thread.currentThread().getName(); 17 System.out.println(tname + "线程运行中...."); 18 for (int i = 0; i < 5; i++) { 19 System.out.println(x); 20 try { 21 Thread.sleep(700); 22 23 } catch (InterruptedException e) { 24 e.printStackTrace(); 25 } 26 } 27 } 28 29 /** 30 * 使用线程 31 * 1、使用实现Runnalbe的类 初始化一个Thread对象 32 * 2、调用start() 33 * 34 * @param args 35 */ 36 public static void main(String[] args) { 37 Thread t1 = new Thread(new ThreadRunnable(1)); 38 Thread t2 = new Thread(new ThreadRunnable(10)); 39 // t1.run();// 如果是调用thread的run方法,则只是一个普通的方法调用,不会开启新的线程 40 t1.start(); 41 t2.start(); 42 } 43 }
运行结果
启动了两个线程,可以看到线程交替运行的情况。
(二)Java同步关键字
-
synchronized
应用方式:
(1)修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
(2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
(3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
示例代码
1 public class SyncTest { 2 public static void main(String[] args) { 3 final SyncTest mySynchronized = new SyncTest(); 4 final SyncTest mySynchronized2 = new SyncTest(); 5 new Thread("thread1") { 6 public void run() { 7 synchronized (mySynchronized) { 8 try { 9 System.out.println(this.getName()+" start"); 10 // int i =1/0; //如果发生异常,jvm会将锁释放 11 Thread.sleep(5000); 12 System.out.println(this.getName()+"醒了"); 13 System.out.println(this.getName()+" end"); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 } 19 }.start(); 20 new Thread("thread2") { 21 public void run() { 22 synchronized (mySynchronized2) { //争抢同一把锁时,线程1没释放之前,线程2只能等待 23 // synchronized (mySynchronized2) { //如果不是一把锁,可以看到两句话同时打印 24 System.out.println(this.getName()+" start"); 25 System.out.println(this.getName()+" end"); 26 27 } 28 } 29 }.start(); 30 } 31 }
运行结果
-
volatile
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级。
-
lock
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); }
Lock接口中每个方法的使用:
lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的。 unLock()方法是用来释放锁的。
四个获取锁方法的区别:
- lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
2.tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
3. tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
4.lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
ReentrantLock
直接使用lock接口的话,我们需要实现很多方法,不太方便,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法,ReentrantLock,意思是“可重入锁”。
以下是ReentrantLock的使用案例:
例子1,lock()的正确使用方法
1 package thread.syncAndlock; 2 3 import java.util.ArrayList; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 public class MyLockTest { 8 9 private static ArrayList<Integer> arrayList = new ArrayList<Integer>(); 10 static Lock lock = new ReentrantLock(); // 注意这个地方 11 12 public static void main(String[] args) { 13 new Thread("thread0"){ 14 @Override 15 public void run() { 16 Thread thread = Thread.currentThread(); 17 lock.lock(); 18 try { 19 System.out.println(thread.getName() + "得到了锁"); 20 for (int i = 0; i < 5; i++) { 21 arrayList.add(i); 22 } 23 24 } catch (Exception e) { 25 // TODO: handle exception 26 } finally { 27 System.out.println(thread.getName() + "释放了锁"); 28 System.out.println(arrayList); 29 lock.unlock(); 30 } 31 } 32 }.start(); 33 34 new Thread("thread1"){ 35 @Override 36 public void run() { 37 Thread thread = Thread.currentThread(); 38 lock.lock(); 39 try { 40 System.out.println(thread.getName() + "得到了锁"); 41 for (int i = 0; i < 5; i++) { 42 arrayList.add(i); 43 } 44 45 } catch (Exception e) { 46 // TODO: handle exception 47 } finally { 48 System.out.println(thread.getName() + "释放了锁"); 49 System.out.println(arrayList); 50 lock.unlock(); 51 } 52 } 53 }.start(); 54 } 55 }
例1执行结果
例子2,tryLock()的使用方法
1 package thread.syncAndlock; 2 3 import java.util.ArrayList; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 public class MyTryLockTest { 8 9 static Lock lock = new ReentrantLock(); // 注意这个地方 10 11 public static void main(String[] args) { 12 new Thread("thread0"){ 13 @Override 14 public void run() { 15 Thread thread = Thread.currentThread(); 16 lock.lock(); 17 try { 18 System.out.println(thread.getName() + "得到了锁"); 19 Thread.sleep(2000); 20 } catch (Exception e) { 21 // TODO: handle exception 22 } finally { 23 System.out.println(thread.getName() + "释放了锁"); 24 lock.unlock(); 25 } 26 } 27 }.start(); 28 29 new Thread("thread1"){ 30 @Override 31 public void run() { 32 Thread thread = Thread.currentThread(); 33 boolean islock = lock.tryLock(); 34 System.out.println(thread.getName() + "lock status " + islock); 35 if(islock==true){ 36 try { 37 System.out.println(thread.getName() + "得到了锁"); 38 } catch (Exception e) { 39 // TODO: handle exception 40 } finally { 41 System.out.println(thread.getName() + "释放了锁"); 42 lock.unlock(); 43 } 44 } 45 } 46 }.start(); 47 } 48 }
执行结果
例子3,lockInterruptibly()响应中断的使用方法:
1 package thread.syncAndlock; 2 3 import java.util.concurrent.locks.Lock; 4 import java.util.concurrent.locks.ReentrantLock; 5 6 public class MyInterruptTest { 7 private Lock lock = new ReentrantLock(); 8 9 public static void main(String[] args) { 10 MyInterruptTest test = new MyInterruptTest(); 11 MyThread thread0 = new MyThread(test); 12 MyThread thread1 = new MyThread(test); 13 thread0.start(); 14 thread1.start(); 15 16 try { 17 Thread.sleep(2000); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 System.out.println("====================="); 22 if(thread0.getState().equals(Thread.State.WAITING)){ 23 thread0.interrupt(); 24 } 25 if(thread1.getState().equals(Thread.State.WAITING)){ 26 thread1.interrupt(); 27 } 28 29 } 30 31 public void insert(Thread thread) throws InterruptedException{ 32 System.out.println(thread.getName()+"准备插入数据"); 33 34 lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出 35 36 try { 37 System.out.println(thread.getName()+"得到了锁,开始插入数据"); 38 long startTime = System.currentTimeMillis(); 39 for( ; ;) { 40 if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) 41 break; 42 //插入数据 43 44 } 45 }finally { 46 System.out.println(Thread.currentThread().getName()+"执行finally"); 47 lock.unlock(); 48 System.out.println(thread.getName()+"释放了锁"); 49 } 50 } 51 } 52 53 class MyThread extends Thread { 54 private MyInterruptTest test = null; 55 public MyThread(MyInterruptTest test) { 56 this.test = test; 57 } 58 @Override 59 public void run() { 60 61 try { 62 test.insert(Thread.currentThread()); 63 } catch (Exception e) { 64 System.out.println(Thread.currentThread().getName()+"获取不到锁被中断"); 65 } 66 } 67 }
运行结果
ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock(); }
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
下面通过几个例子来看一下ReentrantReadWriteLock具体用法。
例子1:假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果
1 package thread.syncAndlock; 2 3 public class MySynchronizedReadWrite { 4 /** 5 * 一个线程又要读又要写,用synchronize来实现的话,读写操作都只能锁住后一个线程一个线程地进行 6 * @author 7 * 8 */ 9 public static void main(String[] args) { 10 final MySynchronizedReadWrite test = new MySynchronizedReadWrite(); 11 12 new Thread(){ 13 public void run() { 14 test.get(Thread.currentThread()); 15 }; 16 }.start(); 17 18 new Thread(){ 19 public void run() { 20 test.get(Thread.currentThread()); 21 }; 22 }.start(); 23 24 } 25 26 public synchronized void get(Thread thread){ 27 long start = System.currentTimeMillis(); 28 int i=0; 29 while(i<=4) { 30 i++; 31 if(i%4==0){ 32 System.out.println(thread.getName()+"正在进行写操作"); 33 }else { 34 System.out.println(thread.getName()+"正在进行读操作"); 35 } 36 } 37 System.out.println(thread.getName()+"读写操作完毕"); 38 } 39 }
运行结果
例子2:改成用读写锁的话:
1 /** 2 * 使用读写锁,可以实现读写分离锁定,读操作并发进行,写操作锁定单个线程 3 * 4 * 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。 5 * 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。 6 * @author 7 * 8 */ 9 public class MyReentrantReadWriteLock { 10 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 11 12 public static void main(String[] args) { 13 final MyReentrantReadWriteLock test = new MyReentrantReadWriteLock(); 14 15 new Thread(){ 16 public void run() { 17 test.get(Thread.currentThread()); 18 test.write(Thread.currentThread()); 19 }; 20 }.start(); 21 22 new Thread(){ 23 public void run() { 24 test.get(Thread.currentThread()); 25 test.write(Thread.currentThread()); 26 }; 27 }.start(); 28 29 } 30 31 /** 32 * 读操作,用读锁来锁定 33 * @param thread 34 */ 35 public void get(Thread thread) { 36 rwl.readLock().lock(); 37 try { 38 long start = System.currentTimeMillis(); 39 40 while(System.currentTimeMillis() - start <= 1) { 41 System.out.println(thread.getName()+"正在进行读操作"); 42 } 43 System.out.println(thread.getName()+"读操作完毕"); 44 } finally { 45 rwl.readLock().unlock(); 46 } 47 } 48 49 /** 50 * 写操作,用写锁来锁定 51 * @param thread 52 */ 53 public void write(Thread thread) { 54 rwl.writeLock().lock();; 55 try { 56 long start = System.currentTimeMillis(); 57 58 while(System.currentTimeMillis() - start <= 1) { 59 System.out.println(thread.getName()+"正在进行写操作"); 60 } 61 System.out.println(thread.getName()+"写操作完毕"); 62 } finally { 63 rwl.writeLock().unlock(); 64 } 65 } 66 }
运行结果:会出现两个进程交错读
注意:
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
Lock和synchronized的选择
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。