首页 文章 精选 留言 我的

精选列表

搜索[并发编程],共10009篇文章
优秀的个人博客,低调大师

Java并发编程-重入锁

章节目录 什么是重入锁 底层实现-如何实现重入 公平与非公平获取锁的区别与底层实现 1.什么是重入锁 1.1 重入锁的定义 重入锁ReentrantLock,支持重入的锁,表示一个线程对资源的重复加锁。 1.2 重入锁的特性 1.重进入 2.非/公平性获取锁 1.3 自定义同步器Mutex 的缺陷 当线程调用Mutex的lock()方法获取锁之后,再次调用lock()方法,该线程将会被 自己阻塞,原因是Mutex在实现tryAcquire(int acquires)方法时没有考虑占有锁 的线程再次获取锁的场景。 1.4 ReentrantLock & synchronized 关键字 1.synchronized 关键字支持隐式的重进入 2.ReentrantLock 在调用lock() 方法时,已经获取到锁的线程,能够再次调用 lock()方法获取到锁而不被阻塞,即可支持重入 1.4 公平性获取锁 公平性 含义 公平性获取锁 在绝对时间上,先对锁进行获取请求的请求一定先被满足,那么这个锁就是公平的 非公平性获取锁 无上述限制 事实上 公平锁机制往往没有非公平性机制获取锁的效率高,因为会牵扯到频繁的上下文切换,但公平锁可以减少饥饿发生的概率,等待越久的请求越能得到优先满足。 2. 底层实现-如何实现重入 重进入是指任意线程在获取到锁之后能够再次获取该锁,而不被阻塞,改特性实现需要解决以下两个问题: 线程再次获取锁 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。 锁的最终释放 线程重复n次获取了锁,随后在第n次释放锁,锁的释放要求锁对于被获取递 增的次数进行递减操作,当计数==0时表示锁已经成功释放。 2.1 可重入锁的源码非公平性获取同步状态(锁)的 nonfairTryAcquire() 方法 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) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } 该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程的再次请求 则将同步状态值计数器进行递增并返回true,表示获取同步状态成功。 释放同步状态(锁) protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } 如果该锁被获取了n次,那么前n-1此tryRelease(int release) 方法必须返回false,而只有同步状态完全释放了,才能返回true。 3.公平与非公平获取锁的区别与底层实现 3.1 公平性获取锁的底层实现 公平性获取锁即按照客观时间顺序,FIFO方式获取同步状态 具体源码如下所示 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && 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); return true; } return false; } 公平性获取同步状态的与非公平性获取同步状态的区别在于hasQueuedPredecessors()方法的使用,即加入了当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早的加入到同步队列(更早的请求获取锁),因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 非公平性获取锁的实现 公平性获取锁保证了锁的获取顺序按照FIFO原则,不会出现线程“饥饿”的现象,但代价是进行大量的线程切换。 非公平性锁虽然可能造成线程饥饿,但是有极少的线程切换,保证了其更大的吞吐量。

优秀的个人博客,低调大师

java并发编程笔记--CopyOnWriteArrayList

1 描述 1) CopyOnWriteArrayList是List的一种线程安全的实现;2) 其实现原理采用”CopyOnWrite”的思路(不可变元素),即所有写操作,包括:add,remove,set等都会触发底层数组的拷贝,从而在写操作过程中,不会影响读操作;避免了使用synchronized等进行读写操作的线程同步;3) CopyOnWrite对于写操作来说代价很大,故不适合于写操作很多的场景;当遍历操作远远多于写操作的时候,适合使用CopyOnWriteArrayList;4) 迭代器以”快照”方式实现,在迭代器创建时,引用指向List当前状态的底层数组,所以在迭代器使用的整个生命周期中,其内部数据不会被改变;并且集合在遍历过程中进行修改,也不会抛出ConcurrentModificationException;迭代器在遍历过程中,不会感知集合的add,remove,set等操作;5) 因为迭代器指向的是底层数组的”快照”,因此也不支持对迭代器本身的修改操作,包括add,remove,set等操作,如果使用这些操作,将会抛出UnsupportedOperationException;6) 相关Happens-Before规则:一个线程将元素放入集合的操作happens-before于其它线程访问/删除该元素的操作; 2 类图 主要角色:1) Iterable接口:定义创建迭代器操作,赋予实现类创建迭代器的能力;2) Collection接口:定义集合基本操作,所有集合类都需实现该接口;3) List接口:定义列表基本操作,所有列表类都需要实现该接口;4) Serializable接口:标记接口,允许对象序列化;5) Cloneable接口:标记接口,允许对象被克隆,如果类没有实现Cloneable接口,调用类对象的clone方法抛出CloneNotSupportedException;6) RandomAccess接口:标记接口,用来表明其支持快速(通常是固定时间)随机访问(比如:ArrayList支持快速随机访问,所以使用普通遍历方式效率更高;而LinkedList则更适合顺序访问,所以使用迭代器的方式效率更高);JDK中推荐的是对List集合尽量要实现RandomAccess接口;7) COWIterator:CopyOnWriteArrayList对应的迭代器;不支持写操作,当调用写操作时,将抛出UnsupportedOperationException异常;8) COWSubList:调用subList方法时,CopyOnWriteArrayList对应的子列表对象,和CopyOnWriteArrayList共享同一个array对象,使用使用offset和size维护对array的位置偏移;且当CopyOnWriteArrayList调用写操作更新array引用时,COWSubList对应的操作会抛出ConcurrentModificationException;9) COWSubListIterator:COWSubList对应的迭代器,底层通过COWIterator实现,使用offset和size维护对array的位置偏移;不支持写操作,当调用写操作时,将抛出UnsupportedOperationException异常; 3 主要实现 3.1 基本属性定义 /** * 互斥锁,用于保护所有修改操作,通过Unsafe进行初始化 */ final transient ReentrantLock lock = new ReentrantLock(); /** * 存放数据的底层数据结构,只能通过getArray()和setArray()两个方法访问; */ private transient volatile Object[] array; /** * <h2>array属性的getter和setter</h2> * <p>声明为非private类型,以便于CopyOnWriteArraySet访问</p> */ final Object[] getArray() { return array; } final void setArray(Object[] array) { this.array = array; } 1) lock属性:互斥锁,用于控制CopyOnWriteArrayList所有的写操作的同步,以及对应的COWSubList的所有读写操作的同步;2) array属性:Object[]数组,用于存放集合元素的底层数据结构;array只允许CopyOnWriteArrayList定义的getter和setter方法访问;(原因?) 3.2 构造器定义 /** * <h2>初始化array为空数组</h2> */ public CopyOnWriteArrayList() { setArray(new Object[0]); } /** * <h2>将集合参数中的元素作为当前集合对象的元素</h2> */ public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) { elements = ((CopyOnWriteArrayList<?>) c).getArray(); } else { elements = c.toArray(); //注:c.toArray()可能不一定返回Object[] if (elements.getClass() != Object[].class) { elements = Arrays.copyOf(elements, elements.length, Object[].class); } } } /** * <h2>将数组参数中的元素作为当前集合对象的元素</h2> */ public CopyOnWriteArrayList(E[] toCopyIn) { //注:传入数组对象并不是传递引用,而是新建一个数组拷贝原有数组 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); } 1) 定义了3个构造函数,第一个构造函数为无参构造函数,默认初始化array为空数组;后两个构造函数分别接收Collection对象和数组对象作为参数,使用参数的元素作为初始化array的元素;2) 使用数组对象作为参数时,需要通过浅拷贝的方式初始化array,而非直接使用数组对象的引用; 3.3 查找元素 @Override public int size() { return getArray().length; } @Override public boolean isEmpty() { return size() == 0; } private static boolean eq(Object o1, Object o2) { return (o1 == null) ? o2 == null : o1.equals(o2); } private static int indexOf(Object o, Object[] elements, int index, int fence) { if (o == null) { for (int i = index; i < fence; i++) { if (elements[i] == null) { return i; } } } else { for (int i = index; i < fence; i++) { if (o.equals(elements[i])) { return i; } } } return -1; } private static int lastIndexOf(Object o, Object[] elements, int index) { if (o == null) { for (int i = index; i >= 0; i--) { if (elements[i] == null) { return i; } } } else { for (int i = index; i >= 0; i--) { if (o.equals(elements[i])) { return i; } } } return -1; } @Override public boolean contains(Object o) { Object[] elements = getArray(); return indexOf(o, elements, 0, elements.length) >= 0; } @Override public int indexOf(Object o) { Object[] elements = getArray(); return indexOf(o, elements, 0, elements.length); } public int indexOf(E e, int index) { Object[] elements = getArray(); return indexOf(e, elements, index, elements.length); } public int lastIndexOf(Object o) { Object[] elements = getArray(); return lastIndexOf(o, elements, elements.length - 1); } public int lastIndexOf(E e, int index) { Object[] elements = getArray(); return lastIndexOf(e, elements, index); } 1) CopyOnWriteArrayList的所有读操作都不需要加锁;2) 单个元素的访问,直接通过索引访问底层数组;3) 查找遍历等涉及多个元素的读操作,都会针对array的快照进行,即每次操作开始前使用名为elements的Object[]局部变量存放array当前的引用。在遍历过程中,array发生变化时,遍历操作遍历的依然是原来的快照,从而保证了读操作不需要加锁,写操作也不会发生ConcurrentModificationException异常; 3.4 新增元素 @Override public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } } 1) 所有的新增元素操作都需要使用lock加互斥锁;2) 新增元素需要考虑添加元素的个数(单个/集合),添加元素的位置(末尾/非末尾); 3.5 更新元素 @Override public E set(int index, E element) { //加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); E oldValue = get(elements, index); //待更新元素不是原来的元素,才执行copyOnWrite if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; setArray(newElements); } else { // 注:再次更新引用,为了触发volatile的语义,通知所有线程 setArray(elements); } return oldValue; } finally { //解锁 lock.unlock(); } } 1) 同添加元素操作一样,更新元素也需要使用lock加互斥锁;2) 需要注意的是,即使待更新元素和集合中元素引用相同,也需要执行setArray()操作,以便触发volatile语义,通知所有线程; 3.6 删除元素 @Override public E remove(int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); //计算需要移动的元素个数 int numMoved = len - index - 1; //如果删除元素在数组末尾 if (numMoved == 0) { setArray(Arrays.copyOf(elements, len - 1)); } else { Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { lock.unlock(); } } private boolean remove(Object o, Object[] snapshot, int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] current = getArray(); int len = current.length; //如果此时已经进行过写操作 if (snapshot != current) { //校准索引 FINDINDEX: { //在当前代码获取锁时,有可能len已经小于index,故需要取二者中较小的; int prefix = Math.min(index, len); //情形1:[0,index)区间的元素有删除操作,导致index所指元素前移 for (int i = 0; i < prefix; i++) { //如果元素引用已经替换 if (current[i] != snapshot[i] && eq(o, current[i])) { index = i; break FINDINDEX; } } //情形2:当前数组删除元素较多,导致数组长度小于原先index if (index >= len) { return false; } //情形3:[0,index)区间的元素没有删除操作,index所指元素未移动 if (current[index] == o) { break FINDINDEX; } //情形4:[0,index)区间有添加元素操作,导致index所指元素后移,重新定位元素索引 index = indexOf(o, current, index, len); } } Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); setArray(newElements); return true; } finally { lock.unlock(); } } @Override public boolean remove(Object o) { Object[] snapshot = getArray(); int index = indexOf(o, snapshot, 0, snapshot.length); return index >= 0 && remove(o, snapshot, index); } 1) 删除元素时,同样需要加互斥锁,但出于效率考虑,在加锁前都检测待删除元素是否存在,如果不存在则不加锁,直接返回false;2) 由于判断元素是否存在的操作时未加锁,不能保证删除方法执行过程中,其它线程进行写操作。故在加锁后,依然需要对索引进行校准,增加了删除操作实现难度; 3.7 迭代器实现 @Override public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } @Override public ListIterator<E> listIterator() { return new COWIterator<E>(getArray(), 0); } @Override public ListIterator<E> listIterator(int index) { Object[] elements = getArray(); int len = elements.length; if (index < 0 || index > len) { throw new IndexOutOfBoundsException("Index: " + index); } return new COWIterator<E>(elements, index); } /** * <h2>Copy-On-Write迭代器定义</h2> * <p>限定为final,表示不可以被继承</p> */ static final class COWIterator<E> implements ListIterator<E> { /** * 限定为final,指定为不可变对象,存放arry快照 */ private final Object[] snapshot; /** * 迭代器索引 */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public boolean hasPrevious() { return cursor > 0; } @SuppressWarnings("unchecked") public E next() { if (!hasNext()) { throw new NoSuchElementException(); } return (E) snapshot[cursor++]; } @SuppressWarnings("unchecked") public E previous() { if (!hasPrevious()) { throw new NoSuchElementException(); } return (E) snapshot[--cursor]; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor - 1; } /** * Not supported. Always throws UnsupportedOperationException. * * @throws UnsupportedOperationException always; {@code remove} * is not supported by this iterator. */ public void remove() { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * * @throws UnsupportedOperationException always; {@code set} * is not supported by this iterator. */ public void set(E e) { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * * @throws UnsupportedOperationException always; {@code add} * is not supported by this iterator. */ public void add(E e) { throw new UnsupportedOperationException(); } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] elements = snapshot; final int size = elements.length; for (int i = cursor; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } cursor = size; } } 1) COWIterator迭代器实现是基于快照的,即在调用iterator()方法创建迭代器时,传入array的引用作为快照; 通过一个整型游标记录当前访问到元素的索引;2) 因为迭代器是基于快照的,故所有读操作都无须加锁,且迭代过程中,对CopyOnWriteArrayList集合的修改不会影响到迭代操作,也不会抛出ConcurrentModificationException;3) 注意:因为COWSubList是作为CopyOnWriteArrayList的子列表的,所有操作都依赖于array,所以COWSubList的所有操作都必须检测array是否有改变,并且读写操作都要加锁,以保证写操作替换array的时候,没有读操作同步进行; 4 适用场景 4.1 小数据量,读多写少 数据量较小,读操作尤其是遍历操作远多于写操作时候,适合使用CopyOnWriteArrayList。 5 缺点&权衡点 5.1 写操作耗时更多 不可变对象的每次写操作就要进行一次copy/new操作,带来的性能消耗随着copy的数据量显著增加,包括内存的消耗以及copy/new过程的时间消耗;故不适合copy/new数据量很大,并且写操作很多的场景。 5.2 集合占用内存更多 使用Copy-On-Write,如果短时间有大量读伴随着写,则会有很多”快照”引用得不到释放,占用大量内存。 6 应用案例 7 相关知识点 7.1 迭代器模式 7.2 CopyOnWriteArraySet 8 问题思考 1)CopyOnWriteList如何保证在遍历的过程中修改集合不会触发ConcurrentModificationException?2)为什么CopyOnWriteList的迭代器不支持写操作?3)如下代码为什么不直接使用array,而要通过getArray方法? 4)加锁的代码为什么要按照如下方式写?是要防止锁的引用改变吗? 参考 [Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList]()sun.misc.Unsafe类的使用CopyOnWriteArrayList解读

优秀的个人博客,低调大师

Java并发编程基础-线程简介

章节目录 1.线程定义 2.使用多线程的优势 3.线程优先级 4.线程的状态 5.Daemon 线程 1.线程定义 进程与线程的区别 1.进程是cpu进行资源分配的独立单位,指的是程序在数据集合上的一次运行过程。 2.线程是cpu 进行调度的最小单位,在一个进程中会创建多个线程。 线程拥有的独立资源 栈中数据是线程独享的,包括局部变量、程序计数器等 堆中数据是线程共享的,如线程同时操作堆中某对象的某属性。 Java程序运行的实质 一个程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程共同运行 2.使用多线程的优势 1.充分利用更多的处理核心 2.更快的响应时间 例如,一笔订单的创建,它包括插入订单数据,生成订单快照,发送邮件通 知买家和记录货品销售数量等, 用户从单击“订购按钮" 开始,就要等待这些操 作全部完成才能看到订购成功的结果,但是这么多的业务操作,如何才能够跟快的完成? 在上面的场景中,我们可以使用多线程技术,即将数据一致性不强的操作派发 给其他线程处理,好处是响应用户请求的线程能更快的处理完成,缩短了响应时间,提升了用户体验。 3.线程优先级 thread.setPriority(10),线程优先级从1-10顺序排列 4.线程的状态 Java线程在运行的声明周期中可能处于如下表所示的6中状态,在给定的一个时刻,线程只能处于其中一个状态。 状态名称 说明 new 初始状态,线程被构建,但是还没有调用start()方法 runnable 运行状态,Java线程将操作系统中的就绪与运行两种状态统称为"运行中" block 阻塞状态,表示线程等待资源可用,如i/o 或者阻塞于锁 waiting 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) time_waiting 超时等待状态,该状态不同于waiting,它是可以在指定的时间自行返回的 terminated 终止状态,表示当前线程执行完毕 如下图所示,为java线程状态变迁图: java线程状态变迁图 1.线程创建之后,调用start()方法,状态变更为可运行状态,待资源准备就绪后,开始运行。 2.线程执行 lockObject.wait() 方法,线程进入等待状态。 3.进入等待状态的线程依靠其他线程的通知才能返回到运行状态。 4.超时等待相当于在等待状态基础上增加超时限制,超时时间到达会自动返回到运行状态。 5.线程调用同步方法,在没有获取锁的情况下,线程会进入到阻塞状态。 6.线程在执行Runable 的run()方法后,进入终止状态。 Daemon线程 支持性线程,被用作程序中后台调度以及支持性工作。当一个Java虚拟机中不存在非Daemon线程时,JVM将退出。 可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。 Daemon属性需要在启动线程前执行,不能在启动线程之后启动。 注意:Daemon线程被用作完成支持性工作,但在Java虚拟机退出时,Daemon线程中的finally不一定会执行。 如下代码所示: public class Daemon { static class DaemonRunner implements Runnable { public void run(){ try{ TimeUnit.Second.sleep(10);//沉睡10s }finally{ System.out.println("Daemon thread finally run");//执行类似资源回收动作 } } } } 当JVM中已经没有非Daemon线程,虚拟机就要退出。JVM中所有Daemon线程需要立即终止,因此finally块并没有执行。

优秀的个人博客,低调大师

Java并发编程基础-理解中断

章节 什么是中断 中断线程的方法 线程中断状态的判断以及何时被中断的线程所处 isInterrupted() 状态为 false? 1.什么是中断 线程标识位 中断可以理解为线程的一个标识位属性,它标识一个运行中的线程是否被其他线程进行了中断操作。 2.中断线程的方法 其他线程通过调用该线程的 interrupt() 方法对其进行中断操作。 其实就是其他线程对该线程打了个招呼,要求其中断。 3. 线程中断状态的判断 线程通过方法isInterrupted()方法来进行判断是否被中断。 如下两种情况需要注意: 1.如果被中断的线程已经处于终结状态,那么调用该线程对象的 thread.isInterrupted() 返回的仍是 false。 2.在Java API中可以看到,许多抛出 InterruptedException 的方法,(其实线程已经终结了,因为遇到了异常)如Thread.sleep( long mills) 方法)这些方法在抛出InterruptedException 异常之前,JVM会将中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()仍会返回false。 package org.seckill.Thread; import java.util.concurrent.TimeUnit; public class Interrupted { public static void main(String[] args) throws InterruptedException{ Thread sleepThread = new Thread(new SleepRunner(),"sleepRunner"); sleepThread.setDaemon(true);//支持性线程 Thread busyThread = new Thread(new BusyRunner(),"busyRunner"); busyThread.setDaemon(true); sleepThread.start(); busyThread.start(); TimeUnit.SECONDS.sleep(5); sleepThread.interrupt(); busyThread.interrupt(); System.out.println("sleep Thread interrupted status is:"+sleepThread.isInterrupted()); System.out.println("busy Thread interrupted status is:"+busyThread.isInterrupted()); SleepUnit.second(500); } /** * 沉睡中的线程-静态内部类 */ static class SleepRunner implements Runnable { public void run() { while (true) { SleepUnit.second(10); } } } /** * 不停运行,空耗cpu的线程-静态内部类 */ static class BusyRunner implements Runnable { public void run() { while (true) { } } } /** * 静态内部工具类 */ static class SleepUnit { public static void second(int seconds) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } 运行结果: 运行结果 我们可以发现 sleep线程的 isInterrupted 状态为false,其中断标识位被清除了。 busy 线程属于正常中断所以isInterrupted 状态为 true,中断标识位没有被清除。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册