我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解Java中的锁Lock和synchronized

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解Java中的锁Lock和synchronized

一、Lock接口

1、Lock接口和synchronized内置锁

a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁。而synchronized这种内置锁实际上是互斥的,即没把锁最多只能由一个线程持有。

b)Lock接口:Lock接口提供了与synchronized相似的同步功能,和synchronized(隐式的获取和释放锁,主要体现在线程进入同步代码块之前需要获取锁退出同步代码块需要释放锁)不同的是,Lock在使用的时候是显示的获取和释放锁。虽然Lock接口缺少了synchronized隐式获取释放锁的便捷性,但是对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制。

2、lock接口使用的一般形式


Lock lock = new ReentrantLock(); //这里可以是自己实现Lock接口的实现类,也可以是jdk提供的同步组件
lock.lock();//一般不将锁的获取放在try语句块中,因为如果发生异常,在抛出异常的同时,也会导致锁的无故释放
try {
}finally {
    lock.unlock(); //放在finally代码块中,保证锁一定会被释放
}

3、Lock接口的方法


public interface Lock {

    
    void lock();

    
    void lockInterruptibly() throws InterruptedException;

    
    boolean tryLock();

    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    
    void unlock();

    
    Condition newCondition();
}

4、相比于synchronized,Lock接口所具备的其他特性

①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁

②能被中断的获取锁lockInterruptibly():获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁

③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false

二、重入锁

1、重入锁的概念

当某个线程请求一个被其他线程所持有的锁的时候,该线程会被阻塞(后面的读写锁先不考虑在内),但是像synchronized这样的内置锁是可重入的,即一个线程试图获取一个已经被该线程所持有的锁,这个请求会成功。重入以为这锁的操作粒度是线程级别而不是调用级别。我们下面说到的ReentrantLock也是可重入的,而除了支持锁的重入之外,该同步组件也支持公平的和非公平的选择。

2、ReentrantLock

a)ReentrantLock实现的可重入性

对于锁的可重入性,需要解决的两个问题就是:

①线程再次获取锁的识别问题(锁需要识别当前要获取锁的线程是否为当前占有锁的线程);

②锁的释放(同一个线程多次获取同一把锁,那么锁的记录也会不同。一般来说,当同一个线程重复n次获取锁之后,只有在之后的释放n次锁之后,其他的线程才能去竞争这把锁)

③ReentrantLock的可重入测试


import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
 public class TestCR {
     Lock lock = new ReentrantLock();
     
     void m1(){
         try{
             lock.lock(); // 加锁
             for(int i = 0; i < 4; i++){
                 TimeUnit.SECONDS.sleep(1);
                 System.out.println("m1() method " + i);
             }
             m2(); //在释放锁之前,调用m2方法
         }catch(InterruptedException e){
             e.printStackTrace();
         }finally{
             lock.unlock(); // 解锁
         }
     }
     
     void m2(){
         lock.lock();
         System.out.println("m2() method");
         lock.unlock();
     }
     
     public static void main(String[] args) {
         final TestCR t = new TestCR();
         new Thread(new Runnable() {
             @Override
             public void run() {
                 t.m1();
             }
         }).start();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 t.m2();
             }
         }).start();
     }
 }

b)下面分析ReentrantLock的部分源码来学习这个同步组件(默认的非公平锁实现)

①首先可以知道ReentrantLock实现Lock接口public class ReentrantLock implements Lock


abstract static class Sync extends AbstractQueuedSynchronizer {
    
    abstract void lock();

    
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();//获取当前线程
        int c = getState(); //获取当前同步状态的值
        if (c == 0) { //如果当前的同步状态还没有被任何线程获取
            if (compareAndSetState(0, acquires)) { //就更新同步状态的值,因为已经有线程获取到同步装填
                setExclusiveOwnerThread(current);//设置同步状态的线程拥有者为当前获取的线程
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {//增加再次获取同步状态的处理逻辑
            int nextc = c + acquires; //如果再次尝试获取同步状态的线程就是当前已经占有同步状态的线程,那么就更新同步状态的值(进行增加操作)
            if (nextc < 0) // 对同步状态的值进行非法判断
                throw new Error("Maximum lock count exceeded");
            setState(nextc); //更新state的值
            return true;
        }
        return false;
    }

    
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases; //对同一线程而言,就是减去相应的获取次数
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false; //返回值
        if (c == 0) { //只有该线程将获取的次数全部释放之后,才会返回true,并且将当前同步状态的持有者设置为null
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c); //更新state
        return free;
    }

        
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

        
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

        
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

        
    final boolean isLocked() {
        return getState() != 0;
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

②通过上面的非公平锁的实现源码可以看到,ReentrantLock实现可重入的逻辑大概上是这样的:

获取逻辑:首先通过nonfairTryAcquire方法增加了对于同一线程再次获取同步状态的逻辑处理(通过判断当前线程是否为已经同步状态的持有者,来决定是否能够再次获取同步状态,如果当前线程是已经获取到同步状态的那个线程,那么就能够获取成功,并且同时以CAS的方式修改state的值)

释放逻辑:对于成功获取到同步状态的线程,在释放锁的时候,通过tryRelease方法的实现可以看出,如果该锁被线程获取到了n次,那么前(n-1)次释放的操作都会返回false,只有将同步状态完全释放才会返回true。最终获取到同步状态的线程在完全释放掉之后,state值为0并且持有锁的线程为null。

c)关于ReentrantLock的公平和非公平实现

①非公平锁

公平和非公平是针对于获取锁而言的,对于公平锁而言获取锁应该遵循FIFO原则,上面我们通过源码分析了非公平锁的实现(对于非公平锁而言,tryAcquire方法直接使用的是ReentrantLock静态内部类Sync的nofairTryAcquire方法)


//非公平锁实现
static final class NonfairSync extends Sync {

    
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

②公平锁实现

公平锁的实现和非公平实现的主要区别就是tryAcquire方法的实现


static final class FairSync extends Sync {

    final void lock() {
        acquire(1); //调用AQS的模板方法实现锁的获取
    }

    
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread(); //获取当前线程
        int c = getState(); //获取当前同步状态的值
        if (c == 0) { //当前同步状态没有被任何线程获取的时候
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { //这个点的主要处理逻辑就是:hasQueuedPredecessors判断当前线程所在的结点是否含有前驱结点,                                  如果返回值为true表示有前驱结点,那么当前线程需要等待前驱结点中的线程获取并释放锁之后才能获取锁,保证了FIFO
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { //支持重入的逻辑,和非公平锁的实现原理相同
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
//hasQueuedPredecessors的处理逻辑
public final boolean hasQueuedPredecessors() {
    // 简单而言,就是判断当前线程是否有前驱结点
    // 当前结点含有前驱结点时候返回true;当前结点为头结点挥着队列为空的时候返回false
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

d)公平锁和非公平锁的测试

①测试目的

验证上面通过源码分析的,非公平锁在获取锁的时候会首先进行抢锁,在获取锁失败后才会将当前线程加入同步队列队尾中,而公平锁则是符合请求的绝对顺序,也就是会按照先来后到FIFO。在下面的代码中我们使用一个静态内部类继承了ReentrantLock并重写等待队列的方法,作为测试的ReentrantLock。然后创建5个线程,每个线程连续两次去获取锁,分别测试公平锁和非公平锁的测试结果


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.junit.Test;

public class TestReentrantLock {
    
    private Lock fairLock = new ReentrantLock2(true);
    private Lock unFairLock = new ReentrantLock2(false);

    @Test
    public void testFair() throws InterruptedException {
        testLock(fairLock); //测试公平锁
    }

    @Test
    public void testUnFair() throws InterruptedException {
        testLock(unFairLock); //测试非公平锁
    }

    private void testLock(Lock lock) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(lock)) {
                public String toString() {
                        return getName();
                }
            };
            thread.setName(i+"");
            thread.start();
        }
        Thread.sleep(12000);
    }

    private static class Job extends Thread {
        private Lock lock;
        public Job(Lock lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            //两次打印当前线程和等待队列中的Threads
            for (int i = 0; i < 2; i++) {
                lock.lock(); //获取锁
                try {
                    Thread.sleep(1000);
                    System.out.println("当前线程=>" + Thread.currentThread().getName() + " " +
                            "等待队列中的线程=>" + ((ReentrantLock2)lock).getQueuedThreads());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); //释放锁
                }
            }
        }

    }

    private static class ReentrantLock2 extends ReentrantLock {
        public ReentrantLock2(boolean fair) {
            super(fair);
        }
        public Collection<Thread> getQueuedThreads() { //逆序打印等待队列中的线程
            List<Thread> list = new ArrayList<Thread>(super.getQueuedThreads());
            Collections.reverse(list);
            return list;
        }
    }


}

②测试非公平锁

由上面的测试结果简单的得到关于非公平锁的一个结论:通过nofairTryAcquire方法可以得到这样一个前提,当一个线程请求一个锁时,判断获取成功的条件就是这个线程获取到同步状态就可以,那么某个刚刚释放锁的线程再次获取到同步状态的几率就会更大一些(当然实验中也出现并非连续两次获取这把锁的情况,比如下面的测试结果)

③测试公平锁

通过分析下面的测试结果,对于使用公平锁而言,即便是同一个线程连续两次获取锁释放锁,在第一次释放锁之后还是会被放在队尾并从队列头部拿出线程进行执行。并没有出现像非公平锁那样连续两次获取锁的那种情况

④由上面的测试可以看出:非公平锁可能导致在队尾的线程饥饿,但是又因为同一个线程在释放锁的时候有更大的概率再次获取到这把锁,那么这样的话线程的切换次数就会更少(这带来的就是更大的吞吐量和开销的减小)。而虽然公平锁的获取严格按照FIFO的规则,但是线程切换的次数就会更多。

三、Synchronized

1、Synchronized作用对象

①对于普通方法,锁的是当前实例对象

②对于静态同步方法,锁的是类的Class对象

③对于同步代码块,锁的是Synchronized括号中的对象

如下所示的三种情况


package cn.source.sync;

public class TestSync01 {
    private static int count = 0;
    private Object object = new Object();

    public void testSyn1() {
        //同步代码块(这里面是锁临界资源,即括号中的对象)
        synchronized (object) {
            System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
        }
    }

    public void testSyn2() {
        //锁当前对象(相当于普通同步方法)
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()
                    +" count =" + count++);
        }
    }

    //普通同步方法:锁当前对象
    public synchronized void testSyn3() {
        System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
    }

    //静态同步方法,锁的是当前类型的类对象(即TestSync01.class)
    public static synchronized void testSyn4() {
        System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
    }

    //下面的这种方式也是锁当前类型的类对象
    public static void testSyn5() {
        synchronized (TestSync01.class) {
            System.out.println(Thread.currentThread().getName()
                    +" count =" + count ++);
        }
    }
}

2、synchronized的实现原理

①Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步代码块是使用monitorenter和monitorexit来实现的,同步方法 并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。monitorenter指令是在编译后插入同步代码块的起始位置,而monitorexit指令是在方法结束处和异常处,每个对象都有一个monitor与之关联,当一个monitor被持有后它就会处于锁定状态。

②synchronized用的锁是存在Java对象头(非数组类型包括Mark Word、类型指针,数组类型多了数组长度)里面的,对象头中的Mark Word存储对象的hashCode,分代年龄和锁标记位,类型指针指向对象的元数据信息,JVM通过这个指针确定该对象是那个类的实例等信息。

③当在对象上加锁的时候,数据是记录在对象头中,对象头中的Mark Word里存储的数据会随着锁标志位的变化而变化(无锁、轻量级锁00、重量级锁10、偏向锁01)。当执行synchronized的同步方法或者同步代码块时候会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管程或者监视器锁)的起始地址。由于每个对象都有一个monitor与之关联,monitor和与关联的对象一起创建(当线程试图获取锁的时候)或销毁,当monitor被某个线程持有之后,就处于锁定状态。

④Hotspot虚拟机中的实现,通过ObjectMonitor来实现的

如图所示,ObjectMonitor中有两个队列(EntryList、WaitSet)以及锁持有者Owner标记,其中WaitSet是哪些调用wait方法之后被阻塞等待的线程队列,EntryList是ContentionList中能有资格获取锁的线程队列。当多个线程并发访问同一个同步代码时候,首先会进入EntryList,当线程获得锁之后monitor中的Owner标记会记录此线程,并在该monitor中的计数器执行递增计算代表当前锁被持有锁定,而没有获取到的线程继续在EntryList中阻塞等待。如果线程调用了wait方法,则monitor中的计数器执行赋0运算,并且将Owner标记赋值为null,代表当前没有线程持有锁,同时调用wait方法的线程进入WaitSet队列中阻塞等待,直到持有锁的执行线程调用notify/notifyAll方法唤醒WaitSet中的线程,唤醒的线程进入EntryList中等待锁的获取。除了使用wait方法可以将修改monitor的状态之外,显然持有锁的线程的同步代码块执行结束也会释放锁标记,monitor中的Owner会被赋值为null,计数器赋值为0。如下图所示

3、锁的种类、升级和对比

a)锁的种类

Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。锁只能升级,不能降级。

b)锁的升级

①偏向锁

如果代码中基本不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息,消除 synchronized 的同步代码结果,使用锁标记的形式记录锁状态。具体的实现方式大概就是:当一个线程访问同步块并获取锁的时候,会在对象头和栈帧的锁记录中存储偏向的线程ID,之后线程在进入和退出同步块的时候不需要使用CAS进行加锁和解锁,只需要测试对象头中的MarkWord中是否存储着当前线程的偏向锁;如果测试成功,就表示线程获取锁成功,如果测试失败需要检查对象头中的MarkWord的偏向锁表示是否设置为1,如果没有设置就使用CAS竞争锁,设置了就以CAS方式将偏向锁设置为当前线程。在 Monitor 中有变量 ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。使用偏向锁可以避免锁的争抢和锁池状态的维护。提高效率。

②轻量级锁

当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。(自旋锁)当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更

③重量级锁

在自旋过程中,为了避免无用的自旋(比如获得锁的线程被阻塞住了),锁就会被升级为重量级锁。在重量级锁的状态下,其他线程视图获取锁的时候都会被阻塞住,只有持有锁的线程释放锁之后才会唤醒那些阻塞的线程,这些线程就开始竞争锁。

4、关于synchronized的其他说明

a)关于同步方法和非同步方法

同步方法只影响 锁定同一个锁对象的同步方法,不影响非同步方法被其他线程调用,也不影响其他所资源的同步方法(简单理解就是锁的不是同一个资源,就不会影响);

b)synchronized是可重入的

同一个线程,多次调用同步代码,锁定同一个对象,是可重入的;

c)关于同步的继承问题

同一个线程中,子类同步方法覆盖父类的同步方法,可以指定调用父类的同步方法(相当于锁的重入)

d)锁与异常

当同步方法出现异常的时候会自动释放锁,不会影响其他线程的执行

e)synchronized锁的是对象,而不是引用

同步代码一旦加锁之后会有一个临时锁引用执行锁对象,和真实的引用无直接关联,在锁释放之前,修改锁对象引用不会影响同步代码块的执行

f)synchronized中的常量问题

在定义同步代码块的时候,不要使用常量对象作为锁对象

以上就是详解Java中的锁Lock和synchronized的详细内容,更多关于Java Lock synchronized的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解Java中的锁Lock和synchronized

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java Synchronized锁的使用详解

在多线程并发问题中,常用Synchronized锁解决问题。本篇文章主要介绍了并发编程中Synchronized锁的用法知识记录,感兴趣的小伙伴可以了解一下
2022-11-13

Java中Lock和Synchronized的区别是什么

这篇文章主要讲解了“Java中Lock和Synchronized的区别是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java中Lock和Synchronized的区别是什么”吧!1.
2023-06-30

深入详解Java中synchronized锁升级的套路

synchronized锁是啥?锁其实就是一个对象,随便哪一个都可以,Java中所有的对象都是锁,换句话说,Java中所有对象都可以成为锁。本文我们主要来聊聊synchronized锁升级的套路,感兴趣的可以收藏一下
2023-05-15

JUC中的Lock锁与synchronized同步代码块问题怎么解决

这篇文章主要介绍“JUC中的Lock锁与synchronized同步代码块问题怎么解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“JUC中的Lock锁与synchronized同步代码块问题怎么解
2023-06-29

Java中延时Lock vs Synchronized的实例分析

本篇文章给大家分享的是有关Java中延时Lock vs Synchronized的实例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。几天前,当我诊断一些 JIT 编译期间奇
2023-06-17

深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

synchronized 和 Reentrantlock多线程编程中,当代码需要同步时我们会用到锁。Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式。显式锁是JDK1.5引入的,这两种
2023-05-30

Java关键字synchronized原理与锁的状态详解

在Java当中synchronized关键字通常是用来标记一个方法或者代码块。本文将通过示例为大家详细介绍一下Synchronized的各种使用方法,需要的可以参考一下
2022-11-13

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录