多线程,cas,aba笔记

Thread类常见方法:

start():启动一个新线程,在新的线程运行 run 方法中的代码
sleep(long n):让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
join():等待线程结束
interrupt():打断线程,如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断正在运行的线程,则会设置 打断标记 ;
interrupted():判断当前线程是否被打断
currentThread():获取当前正在执行的线程

start(),join()使用示例:

复制代码
   //Thread类join()用法
    static Logger log = LoggerFactory.getLogger(Demo01.class);
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        log.info("开始");
//创建t1线程 Thread t1
= new Thread(() -> { log.info("开始"); try { sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } log.info("结束"); r = 10; });
//执行t1线程 t1.start();
//等待t1结束再向下运行 t1.join(); log.info(
"结果为:{}", r); log.info("结束"); }
复制代码

 synchronized和@Transactional搭配使用时需要注意:@Transactional是修饰方法的,如果将synchronized锁加在方法内部,意味着释放锁后事务还未提交,此时其他线程进到这个方法里来获得锁后执行查询等操作,查到的还是上次未提交的数据,很可能发生并发问题.如果synchronized想锁住整个方法,可以加在方法头上,也可以把方法放在synchronized大括号内,加在方法头上相当于这个方法串行执行,很多情况下加锁可以更细致,比如秒杀接口,规定每个用户只能买一件商品,那就没必要给整个秒杀接口加锁,只要防止同一个用户多次下单就好了,可以synchronized(userId.toString().intern()){秒杀方法},这样不同的用户不用等待锁,userId是long类型时,相同的long值可能有不同的string值,所以要加ntern(),为了保证相同的long值有相同的string值.

复制代码
/fun1和fun2是一样的,只是synchronized写法不同,synchronized不能锁方法,fun1只是一种简写,实质还是锁this对象
    public synchronized  void fun1(){}
    public void fun2(){
        synchronized (this){}
    }
//fun3和fun4是一样的,静态方法上用synchronized是锁当前类
    public synchronized static void fun3(){}
    public static void fun4(){
        synchronized (Demo01.class){}
    }
复制代码

synchronized

Object类 wait(),notify(),notifyAll()用法

复制代码
public class Demo02 {
    final static Object lock = new Object();
    //wait(),notify(),notifyAll()用法
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (lock){
                System.out.println("t1执行...");
                    try {
                        lock.wait();//让线程在lock对象上一直等待,期间释放对lock对象的锁定
                      //   lock.wait(1000); //最多等1s,如果到期没被唤醒就自己唤醒
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("t1其他代码");
            }


        },"t1").start();
        new Thread(()->{
            synchronized (lock){
                System.out.println("t2执行...");
                try {
                    lock.wait();//让线程在lock对象上一直等待
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2其他代码");
            }


        },"t2").start();
        try {
            sleep(2000);//主线程睡两秒,确保确保执行下行代码时其他线程已经上锁
            System.out.println("唤醒lock上的其他线程");
            synchronized (lock){
                // lock.notify();//唤醒lock上某一个线程
                lock.notifyAll();//唤醒lock上所有线程
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
复制代码

sleep(long n)和wait(long n)的区别:

1 sleep()是Thread的方法,wait()是object的方法;

2 sleep不强制和synchronized一起用,wait强制和synchronized一起用

3 sleep时不释放锁,wait时释放锁

runnable接口创建多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public void fun() throws InterruptedException {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                logger.debug("running");
            }
        };
        Thread t = new Thread(r,"thread1");
        t.start();
        t.sleep(1000);
        // t.getState(); 获取线程状态
        logger.debug("running2");
//        TimeUnit.SECONDS.sleep(1); 可读性比Thread.sleep(1000)好
 
    }

FutureTask+Callable实现多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
public  void fun1() throws ExecutionException, InterruptedException {
       FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
           @Override
           public Integer call() throws Exception {
               logger.debug("running...");
               Thread.sleep(1000);
               return 100;
           }
       });
       Thread t = new Thread(task,"thread1");
       t.start();
       logger.debug("{}",task.get());
   }

主线程给t1线程设打断标记,t1线程自行选择结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void fun2() throws InterruptedException {
       Runnable r = () -> {
          while(true){
              boolean interrupted =Thread.currentThread().isInterrupted();//获取当前线程的打断标记
              if(interrupted){
                  logger.debug("当前线程的打断标记被其他线程设为true,本线程自行结束");
                  break;
              }
          }
       };
       Thread t = new Thread(r,"thread1");
       t.start();
       Thread.sleep(500);//主线程休息,保证下行代码执行时t1线程在运行
       logger.debug("主线程打断t1线程");
       t.interrupt();
   }

两个线程对同一个变量操作,一个加五千次每次加1,一个减五千次,每次减1,看结果是否改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test02 {
    static int counter=0;
    public static void main(String[] args) throws InterruptedException {
        //两个线程对同一个变量操作,一个加五千次每次加1,一个减五千次,每次减1,看结果是否不变
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0;i<5000;i++){
                    counter++;
                }
            }
        };
        Thread t2 = new Thread("t1"){
            @Override
            public void run() {
 
                for(int i=0;i<5000;i++){
                    counter--;
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();//t1.join方法的作用是等t1线程执行结束后再执行后面的代码,可以加时间参数,表示最多等多久
        t2.join();
       System.out.println("counter: "+counter);//结果并不是每次都是0
        //为什么会这样:因为自增和自减并不是原子性的,需要三条指令才能完成一次自增或自减,多个线程对同一变量进行变更操作,如果在三条指令没结束前就
        //发生线程切换,就会造成这种情况,这就是多个线程对同一个共享变量执行写操作发生线程不安全的实例。
    }
 
}

用synchronized锁解决上面线程不安全的问题

复制代码
public class Test03 {
    static int counter=0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0;i<5000;i++){
                    synchronized (lock){//对自增操作加锁,保证当前线程自增时其他线程不会执行相同的操作
                        counter++;
                    }

                }
            }
        };
        Thread t2 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0;i<5000;i++){
                    synchronized (lock){
                        counter--;
                    }

                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter: "+counter);//结果为0

    }

}
复制代码

用synchronized锁解决上面线程不安全的问题-面向对象的方式

复制代码
public class Test04 {
    //用synchronized锁解决test02的问题-面向对象的方式

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0;i<5000;i++){
                    room.increment();
                }
            }
        };
        Thread t2 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0;i<5000;i++){
                   room.decrement();

                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter: "+room.getCounter());

    }

}
//将共享变量封装到类里
class Room {
    private int counter=0;
    public void increment(){
        synchronized (this){
            counter++;
        }
    }
    public void decrement(){
        synchronized (this){
            counter--;
        }
    }
    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}
复制代码

主线程控制子线程的启动和暂停:

复制代码
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.lang.Thread.sleep;

public class Demo04 {
    static Logger log = LoggerFactory.getLogger(Demo04.class);
    static final Object lock = new Object();//锁对象做好创建成finnal的,防止后期引用变更造成锁失效
    static boolean hasFactor1 = false;//t1线程是否有运行的条件
    static boolean hasFactor2 = false;//t2线程是否有运行的条件

    public static void main(String[] args) throws InterruptedException {
      /*  //wait(),notifyAll()的使用方式总结
        synchronized (lock){
            while(条件不成){
                lock.wait();
            }
            //干活
        }
        唤醒线程:
        synchronized (lock){
            lock.notifyAll();
        }*/
        new Thread(()->{
            synchronized (lock){
                log.info("t1线程后续运行条件是否具备? [{}]" ,hasFactor1);
            while(!hasFactor1){
                log.info("条件不具备,先等待");
                try {
                    lock.wait();//当前线程进入lock对象的waitset队列
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("t1线程后续运行条件是否具备? [{}]" ,hasFactor1);
                if(hasFactor1){
                    log.info("具备条件,可以开始");
                }
            }
            }
        },"t1").start();
        new Thread(()->{
            synchronized (lock){
                log.info("t2线程后续运行条件是否具备? [{}]" ,hasFactor2);
                while(!hasFactor2){
                    log.info("条件不具备,先等待");
                    try {
                        lock.wait();//当前线程进入lock对象的waitset队列
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("t2线程后续运行条件是否具备? [{}]" ,hasFactor2);
                    if(hasFactor2){
                        log.info("具备条件,可以开始");
                    }
                }
            }
        },"t2").start();
        sleep(1000);
        new Thread(()->{
            synchronized (lock){
                hasFactor2=true;
                log.info("t2线程的运行条件已经具备");
                lock.notifyAll();
            }
        },"t2的唤醒线程").start();
    }
}
复制代码

t1线程等待t2线程的返回结果:

复制代码
public class Demo06 {
    //t1线程等待t2线程的返回结果
    static Logger log = LoggerFactory.getLogger(Demo06.class);
    static final Object lock = new Object();
//返回结果  
static List<String> factor ; public static void main(String[] args) { new Thread(()->{ synchronized (lock){ while (factor==null){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } log.info("{}",factor.toString()); } },"t1").start(); new Thread(()->{ synchronized (lock){ log.info("t2线程业务代码..."); //模拟t2线程的返回结果 List list = new ArrayList<String>(); list.add("success!"); //把t2的返回结果放到 GuardedObject中传给t1 factor=list; lock.notifyAll(); } },"t2").start(); } }
复制代码

ReentrantLock用法:

复制代码
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.Thread.sleep;

public class Demo08 {
//一个线程根据情况唤醒另一个线程
static Logger log = LoggerFactory.getLogger(Demo08.class);
static final Object lock = new Object();//锁对象做好创建成finnal的,防止后期引用变更造成锁失效
static boolean hasFactor1 = false;//t1线程是否有运行的条件
static boolean hasFactor2 = false;//t2线程是否有运行的条件
static ReentrantLock room = new ReentrantLock();
static Condition waitCigaretteSet = room.newCondition();//等烟
static Condition waitTakeoutSet = room.newCondition();//等外卖
public static void main(String[] args) throws InterruptedException {

new Thread(()->{
room.lock();
try {
log.info("t1线程后续运行条件是否具备? [{}]" ,hasFactor1);
while(!hasFactor1){
log.info("条件不具备,先等待");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1线程后续运行条件是否具备? [{}]" ,hasFactor1);
if(hasFactor1){
log.info("具备条件,可以开始");
}
}
}
finally {
room.unlock();
}

},"t1").start();
new Thread(()->{
room.lock();
try {
log.info("t2线程后续运行条件是否具备? [{}]" ,hasFactor2);
while(!hasFactor2){
log.info("条件不具备,先等待");
try {
waitTakeoutSet.await();//当前线程在这个对象上等待并释放锁,ReentrantLock可以在多个对象上等待,syncronized只能在一个对象上等待,等其他线程唤醒这个线程后继续向下执行
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t2线程后续运行条件是否具备? [{}]" ,hasFactor2);
if(hasFactor2){
log.info("具备条件,可以开始");
}
}
}
finally {
room.unlock();
}
},"t2").start();
sleep(1000);
new Thread(()->{
room.lock();
try {
hasFactor2=true;
log.info("t2线程的运行条件已经具备");
waitTakeoutSet.signalAll();
}
finally {
room.unlock();
}
},"t2的唤醒线程").start();
sleep(1000);
new Thread(()->{
room.lock();
try {
hasFactor1=true;
log.info("t1线程的运行条件已经具备");
waitCigaretteSet.signalAll();
}
finally {
room.unlock();
}
},"t1的唤醒线程").start();
}
}
复制代码

ReentrantLock和synchronized两种锁对比:

ReentrantLock:可重入(synchronized也可以),可打断(synchronized不能),可设超时时间(synchronized不可以)
ReentrantLock lock = new ReentrantLock();
lock.lock():普通锁
lock.lockInterruptibly(); 当此线程没有获得到锁而等待时,在其他线程里可以用interrupt方法打断此线程
lock.tryLock(1, TimeUnit.SECONDS);1s内如果获取到锁返回true,不到返回false,也可以不加时间参数
ReentrantLock和synchronized一样也有等待队列,如果线程获得锁后需要的运行条件不满足可以先释放锁进入等待队列,synchronized是锁住哪个对象,在哪个对象上等待,如果多个线程锁一个对象,那多个线程只能在这一个对象上等待,唤醒的时候是调用被锁对象的notifyAll()方法,会把在这个对象上等待的所有对象都唤醒.ReentrantLock可以创建多个Condition对象用于线程的等待,唤醒的时候可以指定唤醒哪一个condition上等待的线程,比synchronized等灵活

ReentrantLock解决哲学家就餐死锁问题:

复制代码
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;
public class Demo07{
    //ReentrantLock解决哲学家就餐死锁问题
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("筷子1");
        Chopstick c2 = new Chopstick("筷子2");
        Chopstick c3 = new Chopstick("筷子3");
        Chopstick c4 = new Chopstick("筷子4");
        Chopstick c5 = new Chopstick("筷子5");
        new Philosopher("哲学家1",c1,c2).start();
        new Philosopher("哲学家2",c2,c3).start();
        new Philosopher("哲学家3",c3,c4).start();
        new Philosopher("哲学家4",c4,c5).start();
        new Philosopher("哲学家5",c5,c1).start();
    }
}
class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子='" + name ;

    }
}
class Philosopher extends Thread {
    static Logger log = LoggerFactory.getLogger(Philosopher.class);
    Chopstick left;
    Chopstick right;
 public Philosopher(String name,Chopstick left,Chopstick right){
     super(name);
     this.left=left;
     this.right=right;
 }

    @Override
    public void run() {
        while (true){
            //尝试获得左手筷子
            if(left.tryLock()){
                try {
                    //尝试获得右手筷子
                    if(right.tryLock()){//如果获取不到第二把锁,会走finally释放第一把锁,这样就可以避免死锁
                        try {
                            eat();
                        }
                        finally {
                            right.unlock();
                        }
                    }
                }
                finally {
                    left.unlock();
                }
            }
        }
    }

 private void eat()  {
     log.info("eating...");
     try {
         sleep(500);
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     }
 }

}
复制代码

多线程控制执行顺序,先输出t2再输出t1:

复制代码
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Demo09 {
    //多线程控制执行顺序,先输出t2再输出t1
    static Logger log = LoggerFactory.getLogger(Demo08.class);
    static final Object lock =new Object();
    static boolean t2run = false;
    public static void main(String[] args) {
        Thread t1 =new Thread(()->{
            synchronized (lock){
                while (!t2run){//如果t2线程还没执行,当前线程先释放锁等待
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.info("t1....");
            }
        },"t1");
    Thread t2 = new Thread(()->{
        synchronized (lock){
            log.info("t2....");
            t2run=true;
            lock.notifyAll();
        }
    },"t2");
    t1.start();
    t2.start();
    }
}
复制代码

volatile的可见性,有序性:

t1线程靠boolean变量a控制while循环,在主线程里改变a的值为false,不会使t1线程跳出while循环,因为t1线程自己保存了一个a变量的副本,改变了变量a,t1保存的副本不会改变.

如何解决这个问题?在变量前加volatile,  volatile只能修饰类的成员变量,不能修饰方法内的局部变量,避免其他线程从自己的缓存里找值,只能从主存里取.

代码示例:

复制代码
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Demo10 {
    //用volatile一个线程终止另一个线程
    static Logger log = LoggerFactory.getLogger(Demo10.class);
    public static void main(String[] args) throws InterruptedException {
        TwoPhase twoPhase = new TwoPhase();
        twoPhase.start();
        Thread.sleep(3500);
        log.info("停止监控");
        twoPhase.stop();
    }

}
class TwoPhase{
    static Logger log = LoggerFactory.getLogger(TwoPhase.class);
    //监控线程
    private Thread monitorThread ;
    private volatile boolean stop = false;
    //启动监控线程
    public void start(){
        monitorThread =  new Thread(()->{
            while (true){
                if(stop){
                    log.info("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.info("执行监控记录");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"monitor");
        monitorThread.start();
    }
    //停止监控线程
    public void stop(){
        stop=true;
    }
}
复制代码

 

syncronized修饰的变量和 volatile修饰的变量对比:

syncronized代码块内的变量被改变时其他线程也可以知道,syncronized也具有可见性,synchronized具有可见性,有序性和原子性,volatile有可见性,有序性(禁止指令重排),单线程不用考虑指令重拍问题,多线程才考虑

乐观锁:不会提前加锁,执行更新语句时看当前值是否和当初查询的值相同,相同则这段时间没其他线程修改,不同则已经有其他线程修改.

实现方案有版本号法和cas法:版本号法就是在表里多加一个version字段,每次变更值都把version在查询的值上加1,where条件加上version=当初查询的值,如果查询后有其他线程修改了version,那where条件肯定不成立.这样就避免了并发问题.第二中方法是cas法(数据库的实现),其实是版本号的精简版,把version字段去掉,version字段的功能转移到要修改的字段上.如果并发高的场景使用乐观锁,sql的失败率会很高,因为同一时间只能第一个sql执行成功,其他sql的where条件都不成立,根据经验,update类型sql可以用乐观锁,其他类型的sql,where条件不好写.

CAS(java实现),适合线程数少于cpu核数的情况,因为线程不会阻塞:

复制代码
private int state = 0;
public void doSomething(){
if(state==0){//多线程环境下有线程安全问题
state=1;
//todo
}
}
用cas解决这个线程安全问题
Class Example(){
private volatile int state = 0;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
static{
try{
stateOffset=unsafe.objectFiledOffset(Example.class.getDeclaredField("state"));//获得state变量的偏移量
}
catch(Exception e){
throw new Error(e);
}
}
public void doSomething(){
if(unsafe.compareAndSwapInt(this,stateOffset,0,1)){
//todo
}
}
}
//
AtomicInteger对上面的cas做了简化
import java.util.concurrent.atomic.AtomicInteger;

public class Demo11 {
    private AtomicInteger balance;
    void fun(){
        while(true){
            //获取最新值
            int pre = balance.get();
            //要修改的金额
            int next = pre -10;
            //真正修改
            if(balance.compareAndSet(pre,next)){ //compareAndSet方法具有原子性,如果balance的值是pre则改成next并返回true,否则返回false进行下次while循环
                break;
            }
        }
    }

}
 
复制代码

 

AtomicInteger使用实例:多线程将pdf文件转换成图片,每个线程转换20页,关键代码:
复制代码

File file = new File(pdfPath);//根据文件路径创建file对象
PDDocument doc = Loader.loadPDF(file);//使用pdfbox创建pdf对象
int pageCount = doc.getNumberOfPages();//获取pdf文件总页数
List<Integer> integerList = new ArrayList<>();//如果pdf文件总页数是75,integerList里是20,20,20,15(20就是每个线程转换的页数)
List<CompletableFuture<Void>> futures = new ArrayList<CompletableFuture<Void>>();
AtomicInteger num = new AtomicInteger(-1);
for (Integer integer : integerList) {
futures.add(
CompletableFuture.runAsync(() -> {
for (int i = 0; i < integer; i++) {
BufferedImage image = renderer.renderImage(num.incrementAndGet(), Float.valueOf(big_scale));
}
}, threadPool))
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
.thenRunAsync(() -> logger.info("Ended doing things"));
all.join();
doc.close();
//两层for循环加起来就是pdf总页数,保证不会漏页,四个线程每次生成图片的页数都用AtomicInteger保证了可见性,保证每页只生成一次,但每个线程生成的页数
//不是顺序的,因为线程可能随时切换,只要保证四个线程之间页数不重复就行.
复制代码

 AtomicInteger,AtomicLong,AtomicBoolean可能产生aba问题,如果aba问题对功能不影响可以忽略,如果有影响可以改成AtomicStampedReference

参考文档:https://blog.csdn.net/shuttlepro/article/details/127791275

固定大小的线程池和execute,submit,invokeAll三种执行任务方法的使用

复制代码
@Slf4j
public class Test05 {
    //固定大小的线程池和execute,submit,invokeAll三种执行任务方法的使用
    static Logger logger = LoggerFactory.getLogger(Test05.class);
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(()->{logger.debug("1");});
        sleep(500);
        pool.execute(new Runnable() {
            @Override
            public void run() {
                logger.debug("2");
            }
        });
        sleep(500);
        pool.execute(()->{logger.debug("3");});
        Future<String> future = pool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                logger.debug("submit.....");
                sleep(1000);
                return "ok";
            }
        });
        logger.debug("{}",future.get());
      List<Future<String>> futures=  pool.invokeAll(Arrays.asList(
                ()->{
                    logger.debug("begin");
                    sleep(200);
                    return "1";
                },
                ()->{
                    logger.debug("begin");
                    sleep(200);
                    return "2";
                },
                ()->{
                    logger.debug("begin");
                    sleep(200);
                    return "3";
                }
        ));
      futures.forEach(f->{
          try {
              logger.debug("{}",f.get());
          } catch (InterruptedException e) {
              e.printStackTrace();
          } catch (ExecutionException e) {
              e.printStackTrace();
          }
      });
    }
}
复制代码

 

  

  

  

  

posted @   杨吃羊  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示