java多线程总结

首先明确进程和线程的区别:

  • 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)
  • 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程切换开销小。线程是cpu调度的最小单位

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止

线程在Running的过程中可能会遇到阻塞(Blocked)情况

  •  1.调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  •  2. 调用wait(),使该线程处于等待池,直到notify()/notifyAll(),线程被唤醒被放到锁定池,释放同步锁使线程回到可运行状态(Runnable)
  •  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lockblocked pool ),同步锁被释放进入可运行状态
  •  4.在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

 

在java中要想实现多线程,有两种手段,一种是继承Thread类,另外一种是实现Runable接口。

 

一、扩展java.lang.Thread类

classThread1 extends Thread

{

   在Thread1类里重写run方法。

}

publicstatic void main(String[] args)

{ 

        Thread1 mTh1=newThread1("A"); 

        Thread1 mTh2=newThread1("B"); 

        mTh1.start(); 

        mTh2.start();    

}  

 

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。执行Thread1两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

 

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。

Thread类相关方法:

   //当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)

   publicstatic Thread.yield()

   //暂停一段时间

   publicstatic Thread.sleep() 

   //在一个线程中调用other.join(),将等待other执行完后才继续本线程。    

   publicjoin()

   //后两个函数皆可以被打断

   publicinterrupte()

  Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

二、实现java.lang.Runnable接口

采用Runnable也是非常常见的一种,我们只需要重写run方法即可

class Thread2 implements Runnable{ 

@Override 

    public void run() { 

       try { 

           do something

          }catch (InterruptedException e){ 

               e.printStackTrace(); 

          } 

    }

publicstatic void main(String[] args) { 

        new Thread(newThread2("C")).start(); 

        new Thread(newThread2("D")).start(); 

    }              

}  

两者比较

实现Runnable接口比继承Thread类所具有的优势

  • 1):适合多个相同的程序代码的线程去处理同一个资源
  • 2):可以避免java中的单继承的限制
  • 3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

wait和sleep区别

  • 1 .  Thread类的方法:sleep(),yield()等
  •       Object的方法:wait()和notify()等
  • 2.   sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  • 3.   wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  • 4.   sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

Fork()函数:

      fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

      一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

      在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

      子进程从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端, 父子进程不共享这些存储空间部分,父子进程共享正文段。这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

 

Java的中断

    调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是设置了JVM内部的中断标记,要求线程自己在合适的时机中断自己。

    具体来说,当对一个线程,调用 interrupt() 时,① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

    一个线程如果有被中断的需求,那么就可以这样做。① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)

 

Java实现同步方法---锁(synchronized、volatile关键字和Lock类)

一、synchronized关键字

synchronized 用在方法签名上,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。

synchronized 用在代码块的使用方式:synchronized(obj){//todo codehere}

当线程运行到该代码块内,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会形成互斥。特别的,当obj == this时,表示当前调用该方法的实例对象。

synchronized修饰的对象有以下几种:

  •   修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
public void run() {

     ...

     synchronized(this/obj) {

          // todo your code

     }

     ...

}
  •  修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;此时synchronized关键字不能继承
 public synchronized void run() {

   for (int i = 0; i < 5; i ++) {

      try {

        System.out.println(Thread.currentThread().getName() + ":" +(count++));

         Thread.sleep(100);

      } catch (InterruptedException e) {

         e.printStackTrace();

      }
   }
}
  •   修改一个静态的方法,其作用的范围是整个静态方法,因为静态方法是属于类的而不属于对象的,所以作用的对象是这个类的所有对象;
public synchronized static voidmethod() {

for (int i = 0; i < 5; i ++) {

try {

    System.out.println(Thread.currentThread().getName()+ ":" + (count++));

    Thread.sleep(100);

  }catch (InterruptedException e) {

           e.printStackTrace();

        }
 }
}
  •  修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
class ClassName {

  public void method() {

      synchronized(ClassName.class) {

         // todo

      }
  }
}

 

使用synchronized 代码块相比方法有两点优势:

 

1、可以只对需要同步的使用

2、与wait()/notify()/nitifyAll()一起使用时,比较方便

 

Synchronized底层原理:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

 

等待唤醒机制与synchronized

     所谓等待唤醒机制主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

 

synchronized的可重入性

     从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

Java虚拟机对synchronized的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

偏向锁

         偏向锁是一种针对加锁操作的优化手段,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以对于没有锁竞争的场合,偏向锁有很好的优化效果。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

 轻量级锁

         倘若偏向锁失败,虚拟机会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

         轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(空等待,比如一个空的有限for循环,在自旋的同时重新竞争锁),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除:

         就是把不必要的同步在编译阶段进行移除。

         在jdk1.5以前,我们的String字符串拼接操作其实底层是StringBuffer来实现的,而在jdk1.5之后,是用StringBuilder来拼接的。我们知道,StringBuffer是一个线程安全的类,也就是说两个append方法都会同步,如果这段代码并不存在线程安全问题,这个时候就会把这个同步锁消除。

 

关于CAS机制:

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。

如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

现在我们尝试使用AtomicInteger类:

用AtomicInteger之后,最终的输出结果可以保证是200。并且在某些情况下,代码的性能会比synchronized更好。

而Atomic操作类的底层正是用到了“CAS机制”。

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

 

二、Lock类

Lock接口提供了比synchronized更加广泛的锁定操作。

Lock接口有3个实现它的类:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。

lock必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化。为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。因此,采用lock加锁和释放锁的一般形式如下:

 Lock lock = new ReentrantLock();

//默认使用非公平锁,如果要使用公平锁,需要传入参数true 

   ........ 

   lock.lock(); 

   try { 

         //更新对象的状态 

        //捕获异常,必要时恢复到原来的不变约束 

       //如果有return语句,放在这里 

     finally { 

           lock.unlock();        //锁必须在finally块中释放  

ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
 

 

三、两者的比较

Synchronized:

1.      Java的关键字,在jvm层面上

2.      释放锁的方式:以获取锁的线程执行完同步代码,释放锁或者线程执行发生异常,jvm会让线程释放锁

3.      假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待,互斥锁不能被中断

4.      锁的状态无法判断

5.      可重入、不可中断、非公平、少量同步

Lock:

1.      是一个类

2.      在finally中必须释放锁,不然容易造成线程死锁

3.      当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,线程可中断,可以收到中断的异常,从而有效退出

4.      锁的状态可以判断

5.      可重入、可判断、可公平、大量同步

 

四、volatile关键字

先补充一下概念:Java 内存模型中的可见性、原子性和有序性。

可见性

指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的

原子性

一个操作是不可分割的,那么我们说这个操作时原子操作。

比如a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。

有序性

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

 

Volatile原理:

    当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

    在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。但是volatile没办法保证对变量的操作的原子性。可以通过synchronized或lock,进行加锁,来保证操作的原子性

当一个变量定义为 volatile 之后,将具备两种特性:

1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量的值在线程间传递均需要通过主内存来完成。

2.禁止指令重排序优化。所以volatile能在一定程度上保证有序性。

举个简单的例子:        

//x、y为非volatile变量

//flag为volatile变量

x = 2;       //语句1

y = 0;       //语句2

flag = true; //语句3

x = 4;        //语句4

y = -1;      //语句5 

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、

 

Java线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。

假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

组成部分

1、线程池管理器(ThreadPoolManager):用于创建并管理线程池

2、工作线程(WorkThread): 线程池中线程

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。

4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。

为什么要用线程池:

线程池作用就是限制系统中执行线程的数量。 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

1.降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗

2.提高响应速度:任务到达时不需要等待线程创建就可以立即执行。

3.提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

常见线程池

①newSingleThreadExecutor

单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

②newFixedThreadExecutor(n)

固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

③newCacheThreadExecutor(推荐使用)

可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor

大小无限制的线程池,支持定时和周期性的执行线程

 

线程池的创建

线程池的创建可以通过ThreadPoolExecutor的构造方法实现:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
 

具体解释一下上述参数:

  •     corePoolSize 核心线程池大小
  •     maximumPoolSize 线程池最大容量大小
  •     keepAliveTime 线程池空闲时,线程存活的时间
  •     TimeUnit 时间单位
  •     ThreadFactory 线程工厂
  •     BlockingQueue任务队列
  •     RejectedExecutionHandler 线程拒绝策略

 

线程的提交:

ThreadPoolExecutor的构造方法如上所示,但是只是做一些参数的初始化,ThreadPoolExecutor被初始化好之后便可以提交线程任务,线程的提交方法主要是execute

 Execute->addWorker->Woker类的run方法

用户向线程池提交任务以后,线程池的执行逻辑(execute):

    1如果当前woker数量小于corePoolSize,则新建一个woker并把当前任务分配给该woker线程,成功则返回。

    2如果第一步失败,则尝试把任务放入阻塞队列,如果成功则返回。

    3如果第二步失败,则判断如果当前woker数量小于maximumPoolSize,则新建一个woker并把当前任务分配给该woker线程,成功则返回。

    4如果第三步失败,则调用拒绝策略处理该任务。

addWorker方法主要做的工作就是新建一个Woker线程,加入到woker集合中,然后启动该线程.

woker线程的执行流程就是首先执行初始化时分配给的任务,执行完成以后会尝试从阻塞队列中获取可执行的任务,如果指定时间内仍然没有任务可以执行,则进入销毁逻辑。

注:这里只会回收corePoolSize与maximumPoolSize直接的那部分woker

临界区:

 

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待

进程进入临界区的调度原则是:

  • 1、如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。
  • 2、任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。
  • 3、进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
  • 4、如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。

 

Java高并发:

如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了。为了更好的理解并发和同步,我们需要先明白两个重要的概念:同步和异步。

同步

可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其它的命令。

同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。

异步

执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。即先相应用户请求,然后慢慢去写数据库

将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。

如何处理并发和同步

     处理并发和同同步问题主要是通过锁机制。

     锁机制有两个层面。

一种是代码层次上的,如java中的同步锁,典型的就是同步关键字synchronized,另外一种是数据库层次上的,比较典型的就是悲观锁和乐观锁。

悲观锁一般就是我们通常说的数据库锁机制,乐观锁一般是指用户自己实现的一种锁机制。

悲观锁:

顾名思义,就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制

在整个数据处理过程中,将数据处于锁定状态。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进 行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几 百上千个并发,这样的情况将导致怎样的后果。 乐观锁机制在一定程度上解决了这个问题。

乐观锁

顾名思义, 就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现。

使用数据版本(Version)记录机制实现

这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据

假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。操作员 A 此时将其读出(version=1 ),并从其帐户余额中扣除 $50($100-$50 )。 在操作员A操作的过程中,操作员B也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20( $100-$20 )。操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣 除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作 员 A 的操作结果的可能。

从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

 

大型网站,比如门户网站。在面对大量用户访问、高并发请求方面,基本的解决方案集中在这样几个环节:使用高性能的服务器、高性能的数据库、高效率的编程语言、还有高性能的Web容器。

除此之外还可以使用下面方法处理高并发:

1、 HTML静态化

对于系统中频繁使用数据库查询但是内容更新很小的应用,可以考虑使用html静态化来实现,比如论坛中论坛的公用设置信息,这些信息目前的主流论坛都可以进行后台管理并且存储再数据库中,这些信息其实大量被前台程序调用,但是更新频率很小,可以考虑将这部分内容进行后台更新的时候进行静态化,这样避免了大量的数据库访问请求。

2、 图片服务器分离

图片是最消耗资源的,于是将图片与页面进行分离,这是基本上大型网站都会采用的策略,他们都有独立的图片服务器,甚至很多台图片服务器。这样的架构可以降低提供页面访问请求的服务器系统压力,并且可以保证系统不会因为图片问题而崩溃

3、 数据库集群和库表散列

数据库集群由于在架构、成本、扩张性方面都会受到所采用DB类型的限制,于是从应用程序的角度来考虑改善系统架构,库表散列是常用并且最有效的解决方案。在应用程序中安装业务和应用或者功能模块将数据库进行分离,不同的模块对应不同的数据库或者表,再按照一定的策略对某个页面或者功能进行更小的数据库散列,比如用户表,按照用户ID进行表散列,这样就能够低成本的提升系统的性能并且有很好的扩展性。sohu的论坛就是采用了这样的架构,将论坛的用户、设置、帖子等信息进行数据库分离,然后对帖子、用户按照板块和ID进行散列数据库和表,最终可以在配置文件中进行简单的配置便能让系统随时增加一台低成本的数据库进来补充系统性能。

4、 缓存

经典的缓存+数据库读写的模式

读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应。

更新的时候,先删除缓存,然后再更新数据库。之所以更新的时候只是删除缓存,因为对于一些复杂有逻辑的缓存数据,每次数据变更都更新一次缓存会造成额外的负担,只是删除缓存,让该数据下一次被使用的时候再去执行读的操作来重新缓存,这里采用的是懒加载的策略。举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次;但是这个缓存在1分钟内就被读取了1次,因此每次更新缓存就会有大量的冷数据,对于缓存符合28黄金法则,20%的数据,占用了80%的访问量

 

 

死锁的四个条件及如何避免和预防死锁

 死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

死锁产生的原因:

1. 系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。

2. 进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

死锁的四个必要条件:

互斥条件:一个资源在一段时间内仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能只能是主动释放。

循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

死锁预防:

破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。

破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。

破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值