阿里Java研发面试题『八部曲』——详解多线程、锁

去大厂面试被问懵了...Java面试官的多线程与锁追魂夺命10连问,80%的人都答不上来 趁着金三银四的季节,想着去大厂面试试一下,说不准咱遇到一个大冤种面试官成功上岸了呢。谁知道一上来面试官简历都没看,直接给我来了套并发多线程组合拳,直接把我给干懵了,大佬是我不配,打扰了。 回来后我把面试官的问题复盘了一下,现在就分享给大家,希望对大家面试有所帮助。当然,如果觉得博主总结的不错的话,希望能给个三连支持一下,让更多的人看到,谢谢啦。 1、什么是进程?什么是线程?(一个简单的暖场题) 进程是操作系统分配资源的最小单元 线程是操作系统调度的最小单元,线程隶属于进程。 一个程序至少有一个进程,一个. 阅读详情

在这里插入图片描述

原文链接:阿里Java研发面试题『八部曲』——详解多线程、锁


1、实现多线程的两种方法

实现多线程有两种方法:继承Thread和实现Runnable接口。

继承Thread:
以卖票为例:

public class MyThread extends Thread {
	private static int COUNT = 5;
	private int ticket = COUNT;
	private String name;
	public MyThread(String s){
		name = s;
	}
	@Override
	    public void run() {
		for (int i = 0; i < COUNT; i++){
			if(ticket > 0){
				System.out.println(name + "-->" + ticket--);
			}
		}
	}

测试使用:

MyThread thread1 = new MyThread("thread1");
        MyThread thread2 = new MyThread("thread2");
        thread1.start();
        thread2.start();

输出:

thread1-->5
thread2-->5
thread1-->4
thread2-->4
thread1-->3
thread2-->3
thread1-->2
thread2-->2
thread1-->1
thread2-->1

可以看到,这种方式每个线程自己拥有了一份票的数量,没有实现票的数量共享。下面看实现Runnable的方式:

实现Runnable接口:

public class MyRunnable implements Runnable {
    private static int COUNT = 5;
    private int ticket = COUNT;

    @Override
    public void run() {
        for(int i = 0; i < COUNT; i++){
            if(ticket > 0){
                System.out.println("ticket-->" + ticket--);
            }
        }
    }
}

测试使用:

 MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();

输出:

ticket-->5
ticket-->3
ticket-->2
ticket-->1
ticket-->4

可以看到,实现Runnable的方式可以实现同一资源的共享。
实际工作中,一般使用实现Runnable接口的方式,是因为:

  • 支持多个线程去处理同一资源,同时,线程代码和数据有效分离,体现了面向对象的思想;
  • 避免了Java的单继承性,如果使用继承Thread的方式,那这个扩展类就不能再去继承其他类。

拓展:
Thread的start()和run()方法区别:

start()方法用于启动一个线程,使其处于就绪状态,得到了CPU就会执行,而直接调用run()方法,就相当于是普通的方法调用,会在主线程中直接运行,此时没有开启一个线程。

下列方法中哪个是执行线程的方法? ()

  • A、run()
  • B、start()
  • C、sleep()
  • D、suspend()

正确答案:A

  • run()方法用来执行线程体中具体的内容
  • start()方法用来启动线程对象,使其进入就绪状态
  • sleep()方法用来使线程进入睡眠状态
  • suspend()方法用来使线程挂起,要通过resume()方法使其重新启动

2、访问控制修饰符(新补充)

关于访问控制修饰符,在第一篇总结中已有详细的介绍。但最近在使用String类的一个方法compareTo()的时候,对private修饰符有了新的理解。String类的compareTo方法是用来比较两个字符串的字典序的,其源码如下:

public int compareTo(String anotherString) {
	int len1 = value.length;
	int len2 = anotherString.value.length;
	//重点是这里!!!
	int lim = Math.min(len1, len2);
	char v1[] = value;
	char v2[] = anotherString.value;
	//重点是这里!!!
	int k = 0;
	while (k < lim) {
		char c1 = v1[k];
		char c2 = v2[k];
		if (c1 != c2) {
			return c1 - c2;
		}
		k++;
	}
	return len1 - len2;
}

上面代码逻辑很好理解,我在看到它里面直接使用anotherString.value来获取String的字符数组的时候很奇怪,因为value是被定义为private的,只能在类的内部使用,不能在外部通过类对象.变量名的方式访问。我们平常都是通过String类的toCharArray()方法来获取String的字符数组的,看到上面的这种使用方法,我赶紧在别的地方测试了一下,发现的确是不能直接通过xx.value的方法来获取字符数组。

正如前面所说的,value是被定义为private的,只能在类的内部使用,不能在外部通过类对象.变量名的方式访问。因为compareTo方法就是String类的内部成员方法,compareTo方法的参数传递的就是String对象过来,此时使用“类对象.变量名”的方式是在该类的内部使用,因此可以直接访问到该类的私有成员。自己再模仿String类来测试一下,发现果然如此。。

问题很细节,但是没有一下想通,说明还是对private的修饰符理解不够到位,前面自认为只要是private修饰的,就不能通过“类对象.变量名”的方式访问,其实还是需要看在哪里面使用。

3、线程同步的方法

当我们有多个线程要访问同一个变量或对象时,而这些线程中既有对改变量的读也有写操作时,就会导致变量值出现不可预知的情况。如下一个取钱和存钱的场景:

没有加入同步控制的情形:

public class BankCount {
	private int count = 0;
	//余额
	public void addMoney(int money){
		//存钱
		count += money;
		System.out.println(System.currentTimeMillis() + "存入:" + money);
		System.out.println("账户余额:" + count);
	}
	public void getMoney(int money){
		//取钱
		if(count - money < 0){
			System.out.println("余额不足");
			System.out.println("账户余额:" + count);
			return;
		}
		count -= money;
		System.out.println(System.currentTimeMillis() + "取出:" + money);
		System.out.println("账户余额:" + count);
	}
}

测试类:

public class BankTest {
	public static void main(String[] args) {
		final BankCount bankCount = new BankCount();
		new Thread(new Runnable() {
			//取钱线程
			@Override
			            public void run() {
				while(true){
					bankCount.getMoney(200);
					try {
						Thread.sleep(1000);
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
		).start();
		new Thread(new Runnable() {
			//存钱线程
			@Override
			            public void run() {
				while(true){
					bankCount.addMoney(200);
					try {
						Thread.sleep(1000);
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
		).start();
	}
}

部分打印结果如下:

余额不足
账户余额:0
1462265808958存入:200
账户余额:200
1462265809959存入:200
账户余额:200
1462265809959取出:200
账户余额:200
1462265810959取出:200
账户余额:200
1462265810959存入:200
账户余额:200
1462265811959存入:200
账户余额:200

可以看到,此时有两个线程共同使用操作了bankCount对象中的count变量,使得count变量结果不符合预期。因此需要进行同步控制,同步控制的方法有以下几种:

(1)使用synchronized关键字同步方法

每一个Java对象都有一个内置锁,使用synchronized关键字修饰的方法,会使用Java的内置锁作为锁对象,来保护该方法。每个线程在调用该方法前,都需要获得内置锁,如果该锁已被别的线程持有,当前线程就进入阻塞状态。

修改BankCount 类中的两个方法,如下:

public synchronized void addMoney(int money){
	//存钱
	count += money;
	System.out.println(System.currentTimeMillis() + "存入:" + money);
	System.out.println("账户余额:" + count);
}
public synchronized void getMoney(int money){
	//取钱
	if(count - money < 0){
		System.out.println("余额不足");
		System.out.println("账户余额:" + count);
		return;
	}
	count -= money;
	System.out.println(System.currentTimeMillis() + "取出:" + money);
	System.out.println("账户余额:" + count);
}

运行测试打印如下结果:

余额不足
账户余额:0
1462266451171存入:200
账户余额:200
1462266452171取出:200
账户余额:0
1462266452171存入:200
账户余额:200
1462266453171存入:200
账户余额:400
1462266453171取出:200
账户余额:200
1462266454171存入:200
账户余额:400
1462266454171取出:200
账户余额:200
1462266455171取出:200
账户余额:0

可以看到,打印结果符合我们的预期。

另外,如果我们使用synchronized关键字来修饰static方法,此时调用该方法将会锁住整个类。(关于类锁、对象锁下面有介绍)

(2)使用synchronzied关键字同步代码块

使用synchronized关键字修饰的代码块,会使用对象的内置锁作为锁对象,实现代码块的同步。

改造BankCount 类的两个方法:

public void addMoney(int money){
	//存钱
	synchronized(this){
		count += money;
		System.out.println(System.currentTimeMillis() + "存入:" + money);
		System.out.println("账户余额:" + count);
	}
}
public void getMoney(int money){
	//取钱
	synchronized(this){
		if(count - money < 0){
			System.out.println("余额不足");
			System.out.println("账户余额:" + count);
			return;
		}
		count -= money;
		System.out.println(System.currentTimeMillis() + "取出:" + money);
		System.out.println("账户余额:" + count);
	}
}

(注:这里改造后的两个方法中因为synchronized包含了方法体的整个代码语句,效率上与在方法名前加synchronized的第一种同步方法差不多,因为里面涉及到了打印money还是需要同步的字段,所以全部包含起来,仅仅是为了说明synchronized作用…)

打印结果:

余额不足
账户余额:0
1462277436178存入:200
账户余额:200
1462277437192存入:200
账户余额:400
1462277437192取出:200
账户余额:200
1462277438207取出:200
账户余额:0
1462277438207存入:200
账户余额:200
1462277439222存入:200
账户余额:400
1462277439222取出:200
账户余额:200

可以看到,执行结果也符合我们的预期。

synchronized同步方法和同步代码块的选择:

同步是一种比较消耗性能的操作,应该尽量减少同步的内容,因此尽量使用同步代码块的方式来进行同步操作,同步那些需要同步的语句(这些语句一般都访问了一些共享变量)。但是像我们上面举得这个例子,就不得不同步方法的整个代码块,因为方法中的代码每条语句都涉及了共享变量,因此此时就可以直接使用synchronized同步方法的方式。

(3)使用重入锁(ReentrantLock)实现线程同步

重入性:是指同一个线程多次试图获取它占有的锁,请求会成功,当释放锁的时候,直到重入次数为0,锁才释放完毕。

ReentrantLock是接口Lock的一个具体实现类,和synchronized关键字具有相同的功能,并具有更高级的一些功能。如下使用:

public class BankCount {
	private Lock lock = new ReentrantLock();
	//获取可重入锁
	private int count = 0;
	//余额
	public void addMoney(int money){
		//存钱
		lock.lock();
		try {
			count += money;
			System.out.println(System.currentTimeMillis() + "存入:" + money);
			System.out.println("账户余额:" + count);
		}
		finally{
			lock.unlock();
		}
	}
	public void getMoney(int money){
		//取钱
		lock.lock();
		try {
			if(count - money < 0){
				System.out.println("余额不足");
				System.out.println("账户余额:" + count);
				return;
			}
			count -= money;
			System.out.println(System.currentTimeMillis() + "取出:" + money);
			System.out.println("账户余额:" + count);
		}
		finally{
			lock.unlock();
		}
	}
}

部分打印结果:

1462282419217存入:200
账户余额:200
1462282420217取出:200
账户余额:0
1462282420217存入:200
账户余额:200
1462282421217存入:200
账户余额:400
1462282421217取出:200
账户余额:200
1462282422217存入:200
账户余额:400
1462282422217取出:200
账户余额:200
1462282423217取出:200
账户余额:0

同样结果符合预期,说明使用ReentrantLock也是可以实现同步效果的。使用ReentrantLock时,lock()和unlock()需要成对出现,否则会出现死锁,一般unlock都是放在finally中执行。

synchronized和ReentrantLock的区别和使用选择:

1、使用synchronized获得的锁存在一定缺陷:

不能中断一个正在试图获得锁的线程
试图获得锁时不能像ReentrantLock中的trylock那样设定超时时间 ,当一个线程获得了对象锁后,其他线程访问这个同步方法时,必须等待或阻塞,如果那个线程发生了死循环,对象锁就永远不会释放;
每个锁只有单一的条件,不像condition那样可以设置多个

2、尽管synchronized存在上述的一些缺陷,在选择上还是以synchronized优先:

如果synchronized关键字适合程序,尽量使用它,可以减少代码出错的几率和代码数量 ;(减少出错几率是因为在执行完synchronized包含完的最后一句语句后,锁会自动释放,不需要像ReentrantLock一样手动写unlock方法;)
如果特别需要Lock/Condition结构提供的独有特性时,才使用他们 ;(比如设定一个线程长时间不能获取锁时设定超时时间或自我中断等功能。)
许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁情况;(比如当我们在多线程环境下使用HashMap时,可以使用ConcurrentHashMap来处理多线程并发)。

下面两种同步方式都是直接针对共享变量来设置的:

(4)对共享变量使用volatile实现线程同步

  • volatile关键字为变量的访问提供了一种免锁机制
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
  • 因此每次使用该变量就要重新计算,直接从内存中获取,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

修改BankCount类如下:

public class BankCount {
	private volatile int count = 0;
	//余额
	public void addMoney(int money){
		//存钱
		count += money;
		System.out.println(System.currentTimeMillis() + "存入:" + money);
		System.out.println("账户余额:" + count);
	}
	public void getMoney(int money){
		//取钱
		if(count - money < 0){
			System.out.println("余额不足");
			System.out.println("账户余额:" + count);
			return;
		}
		count -= money;
		System.out.println(System.currentTimeMillis() + "取出:" + money);
		System.out.println("账户余额:" + count);
	}
}

部分打印结果:

余额不足
账户余额:200
1462286786371存入:200
账户余额:200
1462286787371存入:200
账户余额:200
1462286787371取出:200
账户余额:200
1462286788371取出:200
1462286788371存入:200
账户余额:200
账户余额:200
1462286789371存入:200
账户余额:200

可以看到,使用volitale修饰变量,并不能保证线程的同步。volitale相当于一种“轻量级的synchronized”,但是它不能代替synchronized,volitale的使用有较强的限制,它要求该变量状态真正独立于程序内其他内容时才能使用 volatile。volitle的原理是每次线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,以此来保证同步(这种原理方式正如上面例子看到的一样,多线程的条件下很多情况下还是会存在很大问题的)。因此,我们尽量不会去使用volitale。

(5)ThreadLocal实现同步局部变量

使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal的主要方法有:

  • initialValue():返回当前线程赋予当前线程拷贝的局部线程变量的初始值。一般在定义ThreadLocal类的时候会重写该方法,返回初始值;
  • get():返回当前线程拷贝的局部线程变量的值;
  • set(T value):为当前线程拷贝的局部线程变量设置一个特定的值;
  • remove():移除当前线程赋予局部线程变量的值

如下使用:

public class BankCount {
	private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
		protected Integer initialValue() {
			return 0;
		}
		;
	}
	;
	//余额
	public void addMoney(int money){
		//存钱
		count.set(count.get() + money);
		System.out.println(System.currentTimeMillis() + "存入:" + money);
		System.out.println("账户余额:" + count.get());
	}
	public void getMoney(int money){
		//取钱
		if(count.get() - money < 0){
			System.out.println("余额不足");
			System.out.println("账户余额:" + count.get());
			return;
		}
		count.set(count.get() - money);
		System.out.println(System.currentTimeMillis() + "取出:" + money);
		System.out.println("账户余额:" + count.get());
	}
}

部分打印结果:

余额不足
1462289139008存入:200
账户余额:0
账户余额:200
余额不足
账户余额:0
1462289140008存入:200
账户余额:400
余额不足
账户余额:0
1462289141008存入:200
账户余额:600
余额不足
账户余额:0

从打印结果可以看到,测试类中的两个线程分别拥有了一份count拷贝,即取钱线程和存钱线程都有一个count初始值为0的变量,因此可以一直存钱但是不能取钱。

ThreadLocal使用时机:

由于ThreadLocal管理的局部变量对于每个线程都会产生一份单独的拷贝,因此ThreadLocal适合用来管理与线程相关的关联状态,典型的管理局部变量是private static类型的,比如用户ID、事物ID,我们的服务器应用框架对于每一个请求都是用一个单独的线程中处理,所以事物ID对每一个线程是唯一的,此时用ThreadLocal来管理这个事物ID,就可以从每个线程中获取事物ID了。

ThreadLocal和前面几种同步机制的比较:

  • hreadLocal和其它所有的同步机制都是为了解决多线程中的对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。这时该变量是多个线程共享的,使用这种同步机制需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等等很多。所有这些都是因为多个线程共享了资源造成的。
  • ThreadLocal就从另一个角度来解决多线程的并发访问,ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象的特定于线程的状态封装进ThreadLocal。
  • ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。

4、锁的等级:方法锁、对象锁、类锁

Java中每个对象实例都可以作为一个实现同步的锁,也即对象锁(或内置锁),当使用synchronized修饰普通方法时,也叫方法锁(对于方法锁这个概念我觉得只是一种叫法,因为此时用来锁住方法的可能是对象锁也可能是类锁),当我们用synchronized修饰static方法时,此时的锁是类锁。

对象锁的实现方法:

  • 用synchronized修饰普通方法(非static);
  • 用synchronized(this){…}的形式包括代码块;

上面两种方式获得的锁是同一个锁对象,即当前的实例对象锁。(当然,也可以使用其他传过来的实例对象作为锁对象),如下实例:

public class BankCount {
	public synchronized void addMoney(int money){
		//存钱
		synchronized(this){
			//同步代码块
			int i = 5;
			while(i-- > 0){
				System.out.println(Thread.currentThread().getName() + ">存入:" + money);
				try {
					Thread.sleep(500);
				}
				catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public synchronized void getMoney(int money){
		//取钱
		int i = 5;
		while(i-- > 0){
			System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
			try {
				Thread.sleep(500);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

测试类:

public class BankTest {
	public static void main(String[] args) {
		final BankCount bankCount = new BankCount();
		new Thread(new Runnable() {
			//取钱线程
			@Override
			            public void run() {
				bankCount.getMoney(200);
			}
		}
		,"取钱线程").start();
		new Thread(new Runnable() {
			//存钱线程
			@Override
			            public void run() {
				bankCount.addMoney(200);
			}
		}
		,"存钱线程").start();
	}
}

打印结果如下:

取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
取钱线程>取钱:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200
存钱线程>存入:200

打印结果表明,synchronized修饰的普通方法和代码块获得的是同一把锁,才会使得一个线程执行一个线程等待的执行结果。

类锁的实现方法:

  • 使用synchronized修饰static方法
  • 使用synchronized(类名.class){…}的形式包含代码块

因为static的方法是属于类的,因此synchronized修饰的static方法获取到的肯定是类锁,一个类可以有很多对象,但是这个类只会有一个.class的二进制文件,因此这两种方式获得的也是同一种类锁。

如下修改一下上面代码的两个方法:

public void addMoney(int money){
	//存钱
	synchronized(BankCount.class){
		int i = 5;
		while(i-- > 0){
			System.out.println(Thread.currentThread().getName() + ">存入:" + money);
			try {
				Thread.sleep(500);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
public static synchronized void getMoney(int money){
	//取钱
	int i = 5;
	while(i-- > 0){
		System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
		try {
			Thread.sleep(500);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

打印结果和上面一样。说明这两种方式获得的锁是同一种类锁。
类锁和对象锁是两种不同的锁对象,如果将addMoney方法改为普通的对象锁方式,继续测试,可以看到打印结果是交替进行的。

注:

  • 一个线程获得了对象锁或者类锁,其他线程还是可以访问其他非同步方法,获得了锁只是阻止了其他线程访问使用相同锁的方法、代码块;
  • 一个获得了对象锁的线程,可以在该同步方法中继续去访问其他相同锁对象的同步方法,而不需要重新申请锁。

后面一篇,将总结线程池ThreadPool、生产者消费者问题及实现、sleep和wait方法区别。

阿里架构师的架构设计——详解高可用架构设计 推荐阅读 阿里Java研发面试题『八部曲』——从第一步开始 阿里Java研发面试题『八部曲』——基础必备 未完待续… 前言:海恩法则和墨菲定律 海恩法则 事故的发生是量的积累的结果。 再好的技术、再完美的规章 , 在实际操作层面也无法取代人自身的素质和责任心 。 墨菲定律 任何事情都没有表面看起来那么简单 。 所有事情的发展都会比你预计的时间长 。 会出错的事总会出错。 如果你担心某种... 阅读详情

相关推荐

3D点云修复实战:用PCN网络快速补全缺失的激光雷达数据(附代码)

本文详细介绍了PCN(Point Completion Network)网络在3D点云修复领域的实战应用。针对激光雷达数据常见的缺失问题,PCN通过端到端的深度学习架构,直接处理原始点云,实现由粗到精的快速补全。文章深入解析了其核心思想与编码器-解码器架构,并提供了完整的环境搭建、数据准备、模型实现、训练评估及在真实场景下的优化策略与代码示例,为自动驾驶、机器人等领域的开发者提供了高效的数据修复解决方案。

weixin_29041195的博客 392

python基础

想看python的关键字,可以用 import keyword print(keyword.kwlist) >>> print(keyword.kwlist) ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'gl.

m0_55276176的博客 1067

相场断裂模型中的各向异性机制:从裂纹密度到驱动力的数值实现

断裂力学是研究材料在载荷作用下裂纹萌生与扩展规律的基础学科,其核心在于理解材料失效的能量准则与路径选择。传统方法通常预设裂纹路径,而相场断裂模型通过引入连续的相场变量,将裂纹模拟转化为一个能量最小化问题,能够自发预测裂纹的萌生、扩展与分叉,在工程失效分析中具有重要价值。当材料具有显著的方向依赖性时,如单晶、复合材料或轧制金属板,各向同性假设会失效,此时需引入各向异性机制。该机制的核心在于改造两个关键部分:一是裂纹密度函数,通过引入各向异性张量来定义不同方向上形成裂纹微结构的能量成本;二是临界能量释放率(断裂

weixin_31421613的博客 298

我的世界java版正版装mod,吊打面试官

拼多多(三面) 面试前 面完蚂蚁后,早就听闻拼多多这个独角兽,决定也去面一把。首先我在脉脉找了一个拼多多的HR,加了微信聊了下,发了简历便开始我的拼多多面试之旅。这里要非常感谢拼多多HR小姐姐,从面试内推到offer确认一直都在帮我,人真的很nice。 拼多多:一面 为啥蚂蚁只待了三个月?没转正? Java中的HashMap、TreeMap解释下? TreeMap查询写入的时间复杂度多少? HashMap多线程有什么问题? CAS和synchronize有什么区别?都用synchronize不行么? 如

m0_56169789的博客 611

解密UFS Explorer:Drobo BeyondRAID数据恢复实战指南

本文详细介绍了使用UFS Explorer Professional Recovery软件中的Drobo BeyondRAID助手进行数据恢复的完整实战流程。当Drobo设备因BeyondRAID配置信息丢失而无法访问时,该工具能安全、只读地扫描硬盘,虚拟重建存储池,帮助用户找回宝贵数据。指南涵盖了从前期准备、硬盘选择、配置扫描到数据安全导出的每一步关键操作与常见问题解决方案。

weixin_29219539的博客 269

DVWA靶场实战:从搭建到渗透测试的完整指南

本文详细介绍了DVWA靶场的搭建与渗透测试全流程,涵盖LAMP环境搭建、Docker部署及集成环境安装三种方式。通过SQL注入、文件上传漏洞等实战案例,帮助读者掌握Web安全测试技巧,并提供防御方案与进阶调优建议,是网络安全学习的实用指南。

weixin_28312391的博客 489

面试问题——多线程与锁

多线程 一.创建线程的几种方式 继承类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。 (2)创建Thread子类的实例,即创建了线程对象。 (3)调用线程对象的start()方法来启动该线程。 通过Runnable接口创建线程类 (1)定义runnable接口的实现类,并重写该接口的run()方法...

weixin_43895133的博客 659

2026 企查查/天眼查:IP频率限制 + 验证码策略,企业工商数据采集实战

企业工商数据采集技术解析与解决方案 本文深度剖析了2026年企查查和天眼查平台的反爬机制与技术对抗方案。主要内容包括: 反爬机制分析: 采用JA4+ TLS指纹识别、极验v6验证码等多层防护 IP限制策略严格区分未登录/普通/VIP账号 精准识别并封禁数据中心IP 核心解决方案: 构建高可用住宅代理池,动态轮换真实用户IP 基于YOLOv8的验证码缺口识别技术(准确率99%) 分布式架构设计,支持千万级数据采集 多账号池管理与行为模拟技术 技术亮点: 动态住宅IP池实现1%以下封禁率 Playwright浏

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 1327

阿里Java研发面试题『八部曲』——基础必备

欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。 1、List遍历时删除的几种方式比较 1.1、会报错的删除方式: (1)在Iterator遍历时使用list删除 Iterator<String> it = list.iterator(); while(it.hasNext()){ Str...

玩赚AI大模型的博客 320

面向 ABAP 开发者的 Joule 实战:把 AI 带进 Eclipse 的每一次敲键

本文介绍了SAP推出的ABAP开发AI工具套件Joule for developers,重点解析了其代码补全、自然语言对话、代码解释、测试生成等功能如何融入ABAP Cloud开发全流程。文章提供了具体的使用场景和操作示例,包括RAP应用开发中的建模辅助、代码理解、测试生成等环节,并分享了实用的提示词模板。同时强调了启用授权等实施细节,帮助开发者将AI能力转化为实际生产力而非演示效果。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 460

网规第三版:第4章网络资源设备(完结)

第 4 章 网络资源设备 调整内容: 更新主流网络设备(交换机、路由器、防火墙)的技术参数与选型标准 增加国产化网络设备与信创产品的相关内容 精简已淘汰的设备类型与技术指标

鹿鸣天涯 268

阿里Java研发面试题『八部曲』——用实力提升自己

欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。 1、String的split(String regex)方法参数注意点 使用这个方法时,当我们直接以“.”为参数时,是会出错的,如: String str = "12.03"; String[] res = str.spilt("."); //出错!!!...

玩赚AI大模型的博客 207

第182期 Claude Skills:深入探究Anthropic的智能体框架(一篇彻底讲透彻)

摘要: Claude Skills是Anthropic推出的智能体框架,通过连接外部工具和服务扩展Claude AI的功能,支持实时交互与任务执行。其核心架构包含技能路由器、注册表、执行引擎等模块,采用渐进式披露机制优化上下文管理。与MCP和A2A相比,Skills更注重模块化设计与Claude深度集成,通过SKILL.md文件定义可重复工作流程,兼具高效性和灵活性。开发者可创建自定义技能,遵循聚焦用户价值、安全隐私等最佳实践,实现AI能力的场景化拓展。该框架与MCP、A2A形成互补,共同推动智能体生态发展

全网120W+关注AI拉呱,专注人工智能以及科技前沿! 2132

阿里Java研发面试题『八部曲』——从必须掌握的Hash table开始

欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。 1、HashMap、HashTable、ConcurrentHashMap的区别 HashMap和HashTable都实现了Map接口,里面存放的元素不保证有序,并且不存在相同元素; 区别(线程安全和保存值是否为null方面): (1) HashMap和Has...

玩赚AI大模型的博客 111

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

阿里Java研发面试题『八部曲』——从第一步开始

欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。 1、Java变量 Java中主要有如下几种类型的变量 局部变量 类变量(静态变量)-- 属于类 成员变量(非静态变量)-- 属于对象 2、关于枚举 package com.scu.lly; public class EnumTest { /** * 颜...

玩赚AI大模型的博客 369

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Java高级架构师成功的秘诀:先吃透这八部天书

在这里,由于面试中MySQL问的比较多,因此也就在此以MySQL为例为大家总结分享。但是你要学习的往往不止这一点,还有一些主流框架的使用,Spring源码的学习,Mybatis源码的学习等等都是需要掌握的,我也把这些知识点都整理起来了《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!**

2401_84049001的博客 918

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

阿里架构师浅析经典多线程设计模式,你看懂了吗?

推荐阅读 阿里Java研发面试题『八部曲』——从第一步开始 阿里Java研发面试题『八部曲』——基础必备 未完待续… 一、Future模式 思想: 当service(Main方法模拟)请求一个数据的时候,可以先给他返回一个包装类(空壳,代理对象,未来data,FutureData);然后开一个线程去异步加载真实数据,这样当service收到FutrueData,就可以做其他业务逻辑,当要用...

Java_supermanNO1的博客 412

Java高级架构师成功的秘诀:先吃透这八部天书,java分布式开发面试题

本文从基础到高级再到实战,由浅入深,把MySQL讲的清清楚楚,明明白白,这应该是我目前为止看到过最好的有关MySQL的学习笔记了,我相信如果你把这份笔记认真看完后,无论是工作中碰到的问题还是被面试官问到的问题都能迎刃而解!的问题都能迎刃而解![外链图片转存中…(img-wyuu2auR-1711561429788)]

weixin_58134620的博客 428

学海无涯!Java高级架构师成功的秘诀:先吃透这八部天书

最近有粉丝私信我讲,关于Java核心面试知识点的内容希望我们持续更新,部分粉丝已经跳槽完或者是暂时没有跳槽的打算,这些核心知识点可以继续积累。 但是,很多人需要进行更全面详细的技术积累,能够适用与平常工作,而这些是面试资料里面不能一一描述的。 所以,小编应广大粉丝们的需求,推荐一些Java相关的实战型技术书籍,大家可以先收藏起来。 在分享之前,小编先说一下,其实小编看的书很少,因为小编不喜欢看书,一看就打瞌睡。所以这些书目都是从小编的好友那里求过来的,大家可别小看这些书,这可是人家阿里P8大佬成神之前一本本

m0_57541165的博客 247

推荐学java——MyBatis高级,2024年最新含面试题+答案

这里对每个属性的意义都做了解释,其实是完全对应我们之前学习过集合的结构来的,所以理解起来并不难,因为xml中始终都是按照节点来做解析的。我们都知道集合的泛型可以使基本数据类型对应的包装类,也可以是自定义对象类型,那么现在我们就来学习变量对象类型的集合。对应的值通过这种直接写死的方式很不友好,也是不推荐的,实际开发中是有专门的属性文件来管理这些值的,这就要用到。使用起来还是比较灵活的,尤其当我们的SQL比较长的时候,把一些独立的内容提取出来对我们理解程序有很大的意义。这里会分别讲解集合的泛型是。

2401_84411929的博客 949
上一篇: 阿里Java研发面试题『八部曲』——从必须掌握的Hash table开始
下一篇: 二〇一九剑指BAT,Java后端研发面试题解析,连解题思路都告诉你
Java_supermanNO1
博客等级 码龄7年 1170粉丝 329原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值