@[toc]
多线程
一、程序、进程、线程
1、程序
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
2、进程
-
是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 程序是静态的,进程是动态的
3、线程
-
进程可进一步细化为线程,是一个程序内部的一条执行路径
- 若一个进程同一时间 并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间---->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
4、并行与并发
-
区别:
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
二、线程的创建和使用
1、Thread类
1.1、Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
1.2、构造器
Thread()
:创建新的Thread对象Thread(String threadname)
:创建线程并指定线程实例名Thread(Runnable target)
:指定创建线程的目标对象,它实现了Runnable接口中的run方法Thread(Runnable target, String name)
:创建新的Thread对象
2、创建线程的第一种方式(继承Thread类)
-
JDK1.5之前创建新执行线程有两种方法:
继承Thread类的方式
实现Runnable接口的方式
-
继承Thread类
定义子类继承Thread类。
子类中重写Thread类中的run方法。
创建Thread子类对象,即创建了线程对象。
调用线程对象start方法:启动线程,调用run方法。
-
mt子线程的创建和启动过程
3、创建线程和使用的注意点
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常
IllegalThreadStateException
创建线程代码如下:
package com.shsxt.thread;
/**
* 多线程的创建,方式一:继承于Thread类
*/
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
//2. 重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread mt = new MyThread();
//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
mt.start();
//问题一:我们不能通过直接调用run()的方式启动线程。
//mt.run();
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//mt.start();
//我们需要重新创建一个线程的对象
MyThread mt2 = new MyThread();
mt2.start();
//如下操作仍然是在main线程中执行的。
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "******main*******");
}
}
}
}
复制代码
public class ThreadDemo {
public static void main(String[] args) {
//创建Thread类的匿名子类的方式
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100 ; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
复制代码
4、Thread类有关方法
-
void start()
: 启动线程,并执行对象的run()方法 -
run()
: 线程在被调度时执行的操作 -
String getName()
: 返回线程的名称 -
void setName(String name)
:设置该线程名称 -
static Thread currentThread()
: 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类 -
static void yield()
:线程让步暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
-
join()
:在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。 -
static void sleep(long millis)
:(指定时间:毫秒);让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。 -
boolean isAlive()
:返回boolean,判断线程是否还活着
5、线程调度
-
调度策略
- 时间片:
- 抢占式: 高优先级的线程抢占CPU
- 时间片:
-
Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
6、线程的优先级
-
线程的优先级等级:
MAX_PRIORITY :10 MIN _PRIORITY :1 NORM_PRIORITY :5
-
涉及的方法
getPriority() :返回线程优先值 setPriority(int newPriority) :改变线程的优先级
-
注意点:
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
关于方法的一些使用,代码如下:
package com.shsxt.thread;
class HelloThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":" + getPriority() + ":" + i);
}
//yield()方法的使用(礼让线程)
// if (i % 20 == 0) {
// yield();
// }
}
}
public HelloThread(String name) {
//给子线程赋名字
super(name);
}
}
public class ThreadMethod {
public static void main(String[] args) {
//第一种方式:给子线程赋名字
HelloThread h1 = new HelloThread("Thread:1");
//第二种方式:给子线程赋名字
//h1.setName("线程一");
//给子线程设置优先级
//h1.setPriority(Thread.MAX_PRIORITY);
//启动子线程
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
//给主线程设置优先级
//Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
//调用join()方法,当i==20时主线程阻塞,子线程运行完后,主线程才运行
if (i == 20){
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//判断线程是否还存活着
System.out.println(h1.isAlive());
}
}
复制代码
使用继承Thread类,写一个窗口卖票的练习
class Window extends Thread{
//使用static关键字是防止new Window()每个线程都有100张票
//不使用static关键字的话,需要用到创建线程的第二种方式实现Runnable接口
private static int tickets = 100;
@Override
public void run() {
while (true){
if (tickets>0){
System.out.println(getName()+":卖票,票号为"+tickets);
tickets--;
}else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w = new Window();
Window w1 = new Window();
Window w2 = new Window();
w.setName("窗口一");
w1.setName("窗口二");
w2.setName("窗口三");
w.start();
w1.start();
w2.start();
}
}
复制代码
7、创建线程的第二种方式(实现Runnable接口)
-
实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
线程创建,代码如下:
package com.shsxt.thread;
//1. 创建一个实现了Runnable接口的类
class MyThread1 implements Runnable {
//2、实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MyThread1 myThread1 = new MyThread1();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(myThread1);
t1.setName("线程1");
//5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(myThread1);
t2.setName("线程2");
t2.start();
}
}
复制代码
8、创建线程(继承Thread类和实现Runnable接口)的两种方式的异同
相同点:
两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
不同点:
开发中:优先选择:实现Runnable接口的方式
原因:1、实现了Runnable接口的方式解决了类的单继承性的局限性
2、实现Runnable接口的方式更适合来处理多个线程有共享数据的情况。
使用继承Thread类,写一个窗口卖票的练习:
package com.shsxt.thread;
/**
* 创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
* @author Rainbow
* @date 2020/7/15 10:31
*/
class Window1 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
} else {
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
三、线程的生命周期
- JDK 中用Thread.State 类定义了 线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的 五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
1、线程生命周期图
四、线程的同步
首先举个例子看下:
package com.shsxt.day;
/**
* @author Rainbow
* @date 2020/7/15 9:15
*/
class Window extends Thread {
private static int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(getName() + ":卖票,票号为" + tickets);
tickets--;
} else {
break;
}
}
}
}
/**
* @author Rainbow
*/
public class WindowTest {
public static void main(String[] args) {
Window w = new Window();
Window w1 = new Window();
Window w2 = new Window();
w.setName("窗口一");
w1.setName("窗口二");
w2.setName("窗口三");
w.start();
w1.start();
w2.start();
}
}
复制代码
由此代码看出,发现有线程安全问题:理想状态下
极端状态:
-
由上述代码可以看出出现了线程安全问题
-
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
-
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即使有条线程发生了阻塞,也不能改变,也得等这条线程执行完毕,其他线程才能执行
1、Synchronized的使用方法
-
Java 对于多线程的安全问题提供了专业的解决方式 : 同步机制
-
同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}
-
synchronized 还可以放在方法声明中,表示整个方法为同步方法 。
public synchronized void show (){ …. }
-
-
关于以上名词的说明:
同步的代码:操作共享数据的代码 ----------->(同步的范围)同步的代码不能被同步代码块包含多了,也不能包含少了
***同步监视器(俗称:锁)***:任何一个类的对象,都可以充当锁。要求:多个线程必须共同拥有一把锁
-
同步机制中的锁和注意事项:
1、任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
2、同步方法的锁:静态方法(类名.class)、非静态方法(this)
3、同步代码块:自己指定,很多时候也是指定为this或类名.class
注意事项:
1、必须确保使用同一个资源的 多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
2、 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
-
使用同步方式的优缺点:
优点:解决了线程的安全问题。
缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
2、使用同步代码块的方式解决实现Runnable接口的线程安全问题
- 代码如下:
package com.shsxt.thread;
/**
* 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
* @author Rainbow
* @date 2020/7/15 16:22
*/
class Window1 implements Runnable {
private int ticket = 100;
//使用同步代码块的第一种解决方式
Object obj = new Object();
@Override
public void run() {
while (true) {
// synchronized (obj) {
//使用同步代码块的第二种解决方式
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
// }
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
3、使用同步代码块解决继承Thread类的方式的线程安全问题
package com.shsxt.thread;
/**
* 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
*
* @author Rainbow
* @date 2020/7/15 16:33
*/
class Window2 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
//正确的方式:
// synchronized (obj) {
synchronized (Window2.class) { //Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:
// synchronized (this) {//此时的this代表着t1,t2,t3三个对象
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
// }
}
// }
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
4、使用同步方法解决实现Runnable接口的线程安全问题
package com.shsxt.thread;
/**
* @author Rainbow
* @date 2020/7/15 16:40
*/
class Window3 implements Runnable {
private int ticket = 100;
boolean flag = true;
@Override
public void run() {
while (flag) {
show();
}
}
private synchronized void show() {//同步监视器:this
//相当于下面的方式
// synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
flag = false;
}
// }
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
5、使用同步方法处理继承Thread类的方式中的线程安全问题
package com.shsxt.thread;
/**
* 使用同步方法处理继承Thread类的方式中的线程安全问题
* @author Rainbow
* @date 2020/7/15 16:59
*/
class Window4 extends Thread {
static boolean flag = true;
private static int ticket = 100;
@Override
public void run() {
while (flag) {
show();
}
}
private static synchronized void show() {//同步监视器:Window4.class
//没有static关键字修饰,此时的同步监视器为:t1,t2,t3;此种解决方式是错误的
// private synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
flag = false;
}
// }
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
6、单例设计模式之懒汉式( 线程安全)
package com.shsxt.thread;
/**
* 单例线程安全的懒汉模式
* @author Rainbow
* @date 2020/7/15 17:08
*/
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
//方式一、效率低
// synchronized (Singleton.class) {
// if (instance == null) {
// instance = new Singleton();
// }
// return instance;
// }
//方式二、效率高
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
Singleton instance3 = Singleton.getInstance();
System.out.println(instance);
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
}
}
复制代码
五、线程的死锁问题
-
死锁问题的产生
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
-
解决方法
用专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
-
案例
package com.shnsxt.thread; /** * @author Rainbow * @date 2020/7/15 21:28 */ public class ThreadTest { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); //增大产生死锁的几率 try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(){ @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); } try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } }.start(); } } 复制代码
六、创建线程的第三种方式:Lock锁(JDK5.0提供)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
1、Synchronized与Lock的异同?
-
相同点:
二者都可以解决线程安全问题
-
不同点:
1、synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
2、Lock需要手动的启动同步(lock()方法)紧跟try代码块,同时结束同步也需要手动的实现(unlock()方法)且必须放在finally的首行
-
优先使用顺序
Lock ---> 同步代码块(已经进入了方法体,分配了相应资源)----> 同步方法(在方法体之外)
使用Lock锁的案例:
package com.shnsxt.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Rainbow
* @date 2020/7/15 21:41
*/
class Window implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
//3.调用解锁方法:unlock(),且必须放在finally的首行
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
复制代码
七、对以上知识点的一个小练习
package com.shnsxt.thread;
/**
* 银行有一个账户。
* 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
* @author Rainbow
* @date 2020/7/15 21:52
*/
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱
public synchronized void deposit(double AMB) {//同步监视器:this;虽然说使用继承Thread类慎用this,但是此处的this不代表Customer,而是Account
if (AMB>0){
balance+=AMB;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
//储户
class Customer extends Thread{
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
account.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(0);
Customer customer = new Customer(account);
Customer customer1 = new Customer(account);
customer.setName("甲");
customer1.setName("已");
customer.start();
customer1.start();
}
}
复制代码
package com.shnsxt.thread;
/**
* @author Rainbow
* @date 2020/7/16 9:37
*/
class Blank {
private String accountId;
private double balance;
public Blank(String accountId, double balance) {
this.accountId = accountId;
this.balance = balance;
}
public double getBalance() {
return balance;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Blank{" +
"accountId='" + accountId + '\'' +
", balance=" + balance +
'}';
}
}
class DrawThread extends Thread
以上内容来自于网络,如有侵权联系即删除
相关文章
-
iOS中的网络调试
-
iOS底层-cache_t流程分析
-
Swift 5.3 新特性
-
老司机 iOS 周报 #115 | 2020-06-01
-
我的(FE) iTerm2 配置
-
Homebrew 让你从 Mac 切换到 Linux 更轻松
-
Semo 系列文章之三:插件 semo-plugin-chalk
-
iOS探索 全方位解读Block
上一篇: 常用限流算法与Guava RateLimiter源码解析
下一篇: 我是如何让微博绿洲的启动速度提升30%的