Java基础(七) 多线程

目录

进程与线程

并行与并发

JVM的多线程

实现多线程的两种方式

继承Tread类

实现Runnable接口

匿名内部类实现多线程

线程操作

自定义线程名

休眠线程

守护线程

加入线程

线程同步

同步代码块

互斥

锁与synchronized

同步方法

实战——模拟卖票

死锁

线程安全类


进程与线程

当你运行一个程序,系统就在内存中创建了一个进程(process)。显然程序只是一组指令与数据的静态实体,不运行它的话是没有任何意义的;而进程则是一个动态的实体,它负责将程序所具备的功能展现出来。

一般来说一个程序会具备多个功能,每一个具体的功能就可以理解为线程(Thread),因此一个进程至少需要一个线程。线程是进程的子集,不能够独立执行,必须依存在于进程中。

打个比方,当你运行QQ软件的时候,系统就创建了一个名为QQ的进程。此时你想要一边群语音,一边和好友打字聊天,那么QQ进程就会创建语音聊天和打字聊天两个线程(当然这只是一个比喻,真正QQ运行起来要复杂的多)。 

简而言之,运行一个程序至少有一个进程,一个进程至少有一个线程。

 

并行与并发

并发指的是能够做多件事情,并行指的是可以同时做多件事情。换而言之,并行是并发的子集

从上面的描述中,我们可以知道一个并发程序中应该至少存在两个线程,否则不应该称之为并发。如果程序在单核CPU上运行,那么这些线程将会不停的交替执行;如果程序能够并行执行,那么就一定是运行在多核CPU上(此时程序中的每个线程会分配到一个独立的CPU核上,因此可以同时运行)。

也就是说一个多线程的并发程序,如果没有多核CPU来执行,那么就不能达到并行的效果。简单来说就是

  • 并发(单核CPU):交替做多件事情
  • 并行(多核CPU):同时做多件事情

   

JVM的多线程

在弄清楚前面的概念之后,我们来看这样一个问题:JVM是进程还是线程?答案很明显——JVM是进程,因为Java中存在垃圾回收(Garbage Collection)机制。

当一个对象没有引用指向它时,这个对象会成为垃圾。随着垃圾的不断增加,我们的程序还没有结束运行,此时就会触发GC。即在JVM的主线程(运行main()方法产生的线程,也叫main线程)之外,再创建一个GC线程。

代码演示如下:

public class TestGCAndMain {
	
	public static void main(String[] args) {
		for (int i = 0; i < 1000000; i++) {  // 由于垃圾不会马上回收,所以制造足够多的垃圾。
			new Garbage();
		}
		for (int i = 0; i < 1000; i++) {
			System.out.println("now in main");
		}
	}

}

class Garbage {
	
	@Override
	protected void finalize() {
		System.out.println("collecting garbage");
	}
	
}

运行程序就可以在控制台中看到“now in main”和“collecting garbage”两种语句在控制台中交替打印。

部分打印结果截图:

 

实现多线程的两种方式

Java提供两种方式实现多线程,一种是继承Thread类,另一种是实现Runnable接口。

继承Tread类

一个类继承Thread类之后,需要重写该类的run()方法。run()方法中具体的代码逻辑,就是新创建线程的具体功能——当然你也可以不重写run()方法,这就意味着新线程什么也没做。我们只需要创建Thread子类的实例对象并调用它的start()方法,就可以创建一个新的线程。

:调用run()方法不会开启一个新的线程,只是单纯的调用该方法,调用start()方法才会创建线程。

见下面一个例子:

public class MultiThreadingByThread extends Thread {
	
	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			System.out.println("now in MultiThreadByThread");
		}
	}
	
	public static void main(String[] args) {
		MultiThreadingByThread thread = new MultiThreadingByThread();
//		thread.run(); //  // 调用run()方法不会开启一个新线程,只是单纯的调用该方法
		thread.start();
		for (int i = 0; i < 1000; i++) {
			System.out.println("now in main");
		}
	}

}

部分打印结果截图:

实现Runnable接口

Runnable接口中只有一个run()方法,因此一个类实现Runnable接口之后必须重写run()方法。

public interface Runnable {
    public abstract void run();
}

Thread类有一个重载的构造方法,该方法将Runnable接口作为参数。我们将Runnable实现类的实例对象作为Thread类构造方法的参数传入,再调用Thread类实例的start()方法,就能创建一个新线程。

见下面一个例子:

public class MultiThreadingByRunnable implements Runnable {
	
	@Override
	public void run() {
		for(int i = 0; i < 1000; i++) {
			System.out.println("now in MultiThreadingByRunnable");
		}
	}
	
	public static void main(String[] args) {
		MultiThreadingByRunnable runnable = new MultiThreadingByRunnable();
		Thread thread = new Thread(runnable);
		thread.start();
		for (int i = 0; i < 1000; i++) {
			System.out.println("now in main");
		}
	}

}

部分打印结果截图:

不难发现,即使使用实现Runnable接口的方式实现多线程,依然需要借助于Thread才能创建另一个线程。而且这个线程做的事情,也是重写的run()方法中的代码逻辑。下面我们查看Thread的源码,看看具体实现是怎样的。

public class Thread implements Runnable {
   
    private Runnable target;

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);  
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
            boolean inheritThreadLocals) {
        ...
        this.target = target;
        ...
    }
    ...
}

上面是Thread类中的部分代码。可以看到Runnable实现类的实例对象作为参数传递给Thread类的构造方法之后,该对象会被赋值给Thread类的成员变量target。

当我们调用start()方法时,该线程要做事情就是run()方法的代码逻辑,而此时target不为null,那么真正调用是target的run()方法,也就是Runnable实现类重写的run()方法。

匿名内部类实现多线程

通过前面的两个例子可以发现,不论是继承Thread类还是实现Runnable接口,我们仅仅是重写了run()方法,这种情况下使用匿名内部类会更简洁一些:

public class TestMultiThreadingByAnonymous {
	
	public static void main(String[] args) {
		// 匿名Thread子类
		new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("now in thread-1");
				}				
			};
		}.start();
		// 匿名类实现Runnable接口
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("now in thread-2");
				}		
			}
		}).start();
	}

}

部分打印结果截图:

 

线程操作

创建一个线程很简单,难的是线程创建完毕后如何去控制它。

自定义线程名

每个人都有名字,每一个线程也有自己的名字。

public class TestThreadName {
	
	public static void main(String[] args) {
		new Thread(){
			@Override
			public void run() {
				System.out.println(this.getName());
			}
		}.start();
	}
	
}

打印结果如下:

Thread-0

可以看到我们创建的这个线程的名字是“Thread-0”。我们创建的线程默认名字是“Thread-”加上一个数字(从0开始,依次递增),当然线程的名字是可以自定义的。

自定义线程名有两种做法:一种是创建Thread对象时,直接传入线程名字符串作为该线程的名字;另一种是调用setName()方法修改线程的名字。

public class ChangeThreadName {

	public static void main(String[] args) {
		// 创建Thread实例对象时传入线程名
		new Thread("线程A") {	
			@Override
			public void run() {
				System.out.println(this.getName());
			}
		}.start();
		// 通过setName()方法修改线程名
		Thread thread = new Thread() {
			@Override
			public void run() {
				System.out.println(this.getName());
			}
		};
		thread.setName("线程B");
		thread.start();
	}

}

打印结果如下:

线程A
线程B

在上面的例子中,我们可以通过this.getName()来获取当前线程的名字,因为这里是通过Thread的子类实例对象创建线程,所以可以通过this调用Thread中的方法。那么如果是通过实现Runnable接口创建线程,还能通过this获取线程的名字吗?

答案肯定是不行的。因为this代表当前对象的引用,通过实现Runnable接口创建线程,this指向的是Runnable实现类的实例对象,所以无法通过this调用setName()方法。这里我们需要用到Thread类提供静态方法currentThread(),该方法会返回当前正在运行的线程。

public class ChangeThreadNameByCurrentThread {

	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				// 通过Thread.currentThread()获取当前正在运行的线程
				System.out.println(Thread.currentThread().getName());
			}
		}, "线程A").start();
		// currentThread()方法调用时,位于哪个线程中,返回的就是哪个线程
		System.out.println(Thread.currentThread().getName());
	}

}

打印结果如下:

main
线程A

休眠线程

Thread类的静态方法sleep()可以让线程暂时停止运行一段时间(单位毫秒)。和currentThread()方法一样,在哪个线程中调用该方法就会休眠哪个线程。调用该方法会抛出InterruptedException异常。

public class TestSleepThread {

	public static void main(String[] args) throws InterruptedException {
		for (int i = 1; i <= 5; i++) {
			Thread.sleep(1000);  // 休眠一秒
			System.out.println("已经运行" + i + "秒");
		}
	}
	
}

打印结果如下:

已经运行1秒
已经运行2秒
已经运行3秒
已经运行4秒
已经运行5秒

需要注意的是在自己创建的线程中调用sleep()方法时,不能抛出InterruptedException异常,必须try...catch。因为父类 / 接口中的run()方法没有抛出异常,则重写的run()方法中也不能抛出异常(具体可以参考 Java基础(六) 异常)。

public class TestThreadException {

	public static void main(String[] args) {
		new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					// 这里必须使用try...catch捕获异常
					try {
						Thread.sleep(10);	
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
	}

}

守护线程

Java中线程可以分为两类:用户线程(普通线程)、守护线程(后台线程)。

正如其名,守护线程的作用就是服务用户线程。如果已经没有任何一个用户线程还在运行,那么守护线程也就没有可以服务的对象,就会和JVM一起结束运行。守护线程最典型的应用就是GC。

创建守护线程很简单,只要将需要设置为守护线程的线程调用setDaemon(true)即可。需要注意的是,setDaemon()方法必须在start()方法之前调用,因为无法将一个正在运行的线程设置为守护线程。如果你尝试将一个正在运行的线程设置为守护线程,将会抛出IllegalThreadStateException异常。

下面通过一个例子演示:

public class TestDaemonThread {

	public static void main(String[] args) {
		// 守护线程
		Thread daemonThread = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 100; i++) {
					System.out.println(this.getName() + ":守护线程");
				}
			}
		};
		// 用户线程
		new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 3; i++) {
					System.out.println(this.getName() + ":用户线程");
				}
			}
		}.start();
		// setDaemon()方法必须设置在start()之前
		daemonThread.setDaemon(true); 
		daemonThread.start();
	}

}

部分打印结果截图(每次的执行结果会有略微不同):

当用户线程的三条语句打印完成之后,守护线程也会马上结束语句打印。但是守护线程并不是马上结束语句打印,而是接着打印不止一条语句。这是因为当所有的用户线程结束运行,守护线程会先收到结束运行的通知再结束运行,在这个期间内足够守护线程干好些事情了,比如打印几条语句。

比较典型的是使用QQ和好友聊天,如果突然退出QQ,聊天窗口会间隔几秒钟才关闭,这是因为聊天窗口就是一个守护线程。

加入线程

事有轻重缓急,线程也是如此。有的线程要做的事情比较重要,那么该线程就需要“插队”优先执行。

这里需要用到的是join()方法。当一个线程调用join()方法加入到另一个线程时,另一个线程会等待这个加入的线程执行完毕,然后再继续它的工作。

join()还有一个重载的方法,该方法接受一个毫秒值作为参数,这表示被加入的线程最多等待加入线程执行一段时间,如果在这段时间内加入的线程没有执行完毕,那么被加入的线程不会再继续等待加入线程,而是和加入线程争夺CPU的执行权。

public class TestJoinThread {

	public static void main(String[] args) {
		Thread thread = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 5; i++) {
					System.out.println(this.getName() + ": 插队");
				}
			}
		};
		new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 20; i++) {
					if (3 == i) {
						try {
							// 暂停当前线程, 等待该线程至执行结束
							thread.join();	
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.println(this.getName() + ": 正在执行中");
				}
			}
		}.start();
		thread.start();
	}

}

部分打印结果截图(每次的执行结果会有略微不同):

通过打印结果可以看到,线程Thread-0加入线程Thread-1之后,就获得了“插队”的特权。

需要注意的是,如果程序中还存在第三条线程,那么第三条线程并不会停下来等待线程加入线程执行完毕,而是和加入线程争夺CPU的执行权。也就是说,加入线程的“插队”特权是相对于被加入线程而言的

 

线程同步

对于前面给出的例子,多个线程执行的任务之间没有什么关联,因此看不出有什么问题。但是假如两个线程同时操作同一个资源或者数据,而这些操作中既有读操作又有写操作,那么就会导致资源或数据的状态出现混乱。

为了规避这种情况,我们需要用到线程同步。根据同步内容颗粒度大小不同,线程同步分为同步代码块和同步方法。

同步代码块

看这样一个例子。创建两个线程,一个负责读取文件中的数据,另一个负责向文件写入数据,运行程序会怎么样呢?

public class FileAction {
	
	public void read() {
		System.out.print("读");
		System.out.print("取");
		System.out.print("文");
		System.out.print("件");
		System.out.print("\n");
	}
	
	public void write() {
		System.out.print("写");
		System.out.print("入");
		System.out.print("数");
		System.out.print("据");
		System.out.print("\n");
	}
	
	public static void main(String[] args) {
		FileAction p = new FileAction();
		// 线程一:读取文件
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.read();
				}
			}
		}.start();
		// 线程二:写入数据
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.write();
				}
			}
		}.start();
	}

}

在打印结果中,我们会看到类似下面的奇怪的打印信息: 

出现这样的结果正是由于线程不同步引起的。当线程一正在读取文件,CPU就轮换到线程二向文件写入数据,线程二写入数据还没结束,CPU又切换回线程一,这样就导致读取到的数据不正确。

互斥

那么什么情况下需要同步线程呢?相信看到这儿你已经很清楚了,答案是互斥

如果多个线程共享数据或资源,那么它们对共享数据或资源的操作应该是互斥的,也就是说一个线程在对数据或资源进行操作时,其他的线程需要等待该线程执行完毕,然后等待CPU轮换执行。反之,如果两个线程不处理任何共享数据或资源,那么它们不需要以互斥的方式执行。

在前面的例子中,两个线程只有一方独自完成任务,才能保证正确的打印结果。换而言之,两个线程的特定代码块(读写文件)是互斥的。但是如果还存在第三个线程,它的任务是数据计算,那么该线程和前两个线程要做的事情就不是互斥的。即使线程一执行任务的时候被线程三打断,也不会影响到线程一读取文件的结果。

锁与synchronized

Java以的形式实现线程同步。如果多个线程中的代码片段使用同一把锁,那么在某个线程加锁的代码片段执行期间,其他线程在这期间会进入挂起状态。也就是说不同线程中使用同一把锁的代码片段是互斥的

给代码片段加锁需要用到关键字synchronized(同步),使用该关键字需要将任意一个对象作为锁。所以锁也叫对象锁,而加锁的代码片段称之为同步代码块

下面通过一个例子演示:

public class FileActionWithSyncCode {
	
	private Object lock = new Object();
	
	public void read() {
		synchronized(lock) {		// 加锁
			System.out.print("读");
			System.out.print("取");
			System.out.print("文");
			System.out.print("件");
			System.out.print("\n");
		}
	}
	
	public void write() {
		synchronized (lock) {		// 加锁	
			System.out.print("写");
			System.out.print("入");
			System.out.print("数");
			System.out.print("据");
			System.out.print("\n");
		}
	}
	
	public static void main(String[] args) {
		FileActionWithSyncCode p = new FileActionWithSyncCode();
		// 线程一:调用read()方法
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.read();
				}
			}
		}.start();
		// 线程二:调用write()方法
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.write();
				}
			}
		}.start();
	}

}

将互斥的代码片段设置为同步代码块之后,运行程序就不会出现奇怪的打印结果了。

部分打印结果截图:

同步方法

上面的例子中,方法read()和write()中的所有代码都需要设置成同步代码块,那么我们就可以直接将方法read()和write()设置成同步方法。

相较于同步代码块,同步方法的设置简单很多,只需在方法签名上加入synchronized关键字,该方法就是同步方法了。下面将write()方法设置为同步方法:

public class FileActionWithSyncMethod {
	
	private Object lock = new Object();
	
	public void read() {
		synchronized(lock) {		// 同步代码块
			System.out.print("读");
			System.out.print("取");
			System.out.print("文");
			System.out.print("件");
			System.out.print("\n");
		}
	}
	
	public synchronized void write() {  // 同步方法
		System.out.print("写");
		System.out.print("入");
		System.out.print("数");
		System.out.print("据");
		System.out.print("\n");
	}
	
	public static void main(String[] args) {
		FileActionWithSyncMethod p = new FileActionWithSyncMethod();
		// 线程一:调用read()方法
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.read();
				}
			}
		}.start();
		// 线程二:调用write()方法
		new Thread() {
			@Override
			public void run() {
				while(true) {
					p.write();
				}
			}
		}.start();
	}

}

部分打印结果截图(每次的执行结果会有略微不同):

可以看到打印结果中还是存在奇怪的打印信息,这是怎么回事呢?原因很简单——同步方法和同步代码块使用的锁不是同一个。read()方法中同步代码块使用成员变量lock作为锁,但是我们并没有为同步方法设置锁。同步方法使用的锁又是什么呢?

显然我们无法显式的为同步方法加锁,那么程序应该隐式的将某个对象作为同步方法的锁。我们知道要实现方法的互斥,那么两个方法就必须使用同一把锁,也就是说这把锁需要具备唯一性。

在一个类中,有什么东西可以具备唯一性呢?答案是该类的实例对象本身,即this。当该类的实例对象被创建出来,此实例对象就会被默认作为同步方法的锁。

我们将FileActionWithSyncMethod类中read()方法的锁修改为this:

public void read() {
	synchronized(this) {
		System.out.print("读");
		System.out.print("取");
		System.out.print("文");
		System.out.print("件");
		System.out.print("\n");		
	}
}

运行程序,打印结果正常。

这样一来又会出现新的问题——静态方法会随着类的加载而加载,它无法将实例对象作为锁。那么一个类加载的时候什么东西是唯一的呢?没错,就是就是这个类的Class对象。

将FileActionWithSyncMethod类中的read()方法的锁修改为FileActionWithSyncMethod.class,write()方法的修改为静态方法。

public void read() {
	synchronized(FileActionWithSyncMethod.class) {
		System.out.print("读");
		System.out.print("取");
		System.out.print("文");
		System.out.print("件");
		System.out.print("\n");
	}
}

public synchronized static void write() {
	System.out.print("写");
	System.out.print("入");
	System.out.print("数");
	System.out.print("据");
	System.out.print("\n");
}

运行程序,打印结果正确。

总结:

  • 非静态同步方法使用的锁是方法所在类的实例对象
  • 静态同步方法使用的锁是方法所在类的Class对象

 

实战——模拟卖票

相信大家应该都见过卖票窗口,在网络还不发达的年代,窗口前的队伍长龙一度是春运的标志。扯远了,回归正题。

说到卖票,一般有三个特点:一、一天的车票总量是一定的;二、一张票只能卖出一次;三、一个窗口卖票的同时,其余窗口必须等待该窗口卖票完成,才能继续卖票。

假设一共有4个售票窗口,100张票。你可以先试着自己编写程序实现该功能。

下面分别用两种方法实现。继承Thread方式实现:

public class TicketSellerByThread extends Thread {

	private static int num = 100;
	
	public TicketSellerByThread(String name) {
		super(name);
	}

	@Override
	public void run() {
		while (true) {
			synchronized (TicketSellerByThread.class) {
				if (num != 0) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()
							+ ": 卖出1张,第" + --num + "张票");
				}
			}
		}
	}

	public static void main(String[] args) {
		new TicketSellerByThread("窗口1").start();
		new TicketSellerByThread("窗口2").start();
		new TicketSellerByThread("窗口3").start();
		new TicketSellerByThread("窗口4").start();
	}
	
}

实现Runnable接口实现:

public class TicketSellerByRunnable implements Runnable {

	private int num = 100;

	@Override
	public void run() {
		while (true) {
			synchronized (this) {
				if (num != 0) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()
							+ ": 卖出1张,剩余" + --num + "张票");
				}
			}
		}
	}

	public static void main(String[] args) {
		TicketSellerByRunnable ticketSeller = new TicketSellerByRunnable();
		new Thread(ticketSeller, "窗口1").start();
		new Thread(ticketSeller, "窗口2").start();
		new Thread(ticketSeller, "窗口3").start();
		new Thread(ticketSeller, "窗口4").start();
	}
	
}

部分打印结果截图(每次的执行结果会有略微不同):

 

死锁

有这样一个故事:有一群哲学家聚在一起吃饭,菜上齐后准备开动时,他们发现每个人面前只有一只筷子。一只筷子没法夹菜吃饭,但是介于身份的特殊性,他们又做不出手抓饭菜的举动。于是每个人都想说服别人把筷子给自己,结果谁也说服不了别人,于是这群哲学家全部都饿死了。

这个问题描述的情形就是死锁,它是由于同步代码块嵌套而引发的一种状况。看下面一个例子:

public class TestDeadLock {
	
	private static final String chopsticks1 = "筷子1";
	private static final String chopsticks2 = "筷子2";

	public static void main(String[] args) {
		// 线程一
		new Thread() {
			@Override
			public void run() {
				while(true) {
					synchronized (chopsticks1) {
						System.out.println(this.getName()
								+ ": 得到筷子1, 等待筷子2");
						synchronized (chopsticks2) {
							System.out.println(this.getName()
								+ ": 得到筷子2,吃饭");
						}
					}
				}
			}
		}.start();
		// 线程二
		new Thread() {
			@Override
			public void run() {
				while(true) {
					synchronized (chopsticks2) {
						System.out.println(this.getName()
								+ ": 得到筷子2, 等待筷子1");
						synchronized (chopsticks1) {
							System.out.println(this.getName() 
								+ ": 得到筷子1,吃饭");
						}
					}
				}
			}
		}.start();
	}

}

部分打印结果截图(每次的执行结果会有略微不同):

上面的打印结果,程序打印完最后一行语句就不会再继续打印语句了,因为此时已经进入了死锁状态。程序中的死锁具体是怎么产生的呢?下面我们分析死锁产生的过程。

当线程一获得锁chopsticks1的锁定时,线程二也获得了锁chopsticks2的锁定。线程一执行完打印语句后,内层同步代码块想要获得锁chopsticks2的锁定,但是此时锁chopsticks2在线程二手中;另一边线程二执行完打印语句,内层同步代码块想要获得锁chopsticks1的锁定,但是锁chopsticks1在线程一手中。两条线程都因为无法获取需要的锁而进入了挂起状态,死锁由此诞生。

所以在多线程程序中要尽量避免同步代码块的嵌套。

 

线程安全类

在之前的文章中谈到过线程安全(具体可以参考 Java基础(5) 集合):

ArrayList和Vector的底层实现是数组,而LinkedList的底层实现是链表。
所以相对而言,ArrayList和Vector的查找和修改比较快。但Vector出现时间比较早,它的线程是安全的安全,效率相对低一点,目前几乎不用;而ArrayList的线程不安全,所以效率相对快一点。

这里出现的线程安全是什么意思呢?我们分别查看ArrayList和Vector类中的add()方法:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    ...
}

public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    ...
}

通过比较就很容易看出来,二者在功能实现上几乎相同,Vector类所谓的线程安全指的就是使用了同步方法。与之同样的还有Hashtable和HashMap:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    ...
}

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
    public synchronized V put(K key, V value) {
        ...
    }
    ...
}

然而大多数时候我们是不需要线程安全的,如果有需要,我们可以调用Collections工具类提供方法synchronizedCollection(),通过该方法可以将线程不安全的集合对象变成线程安全的。所以Vector和Hashtable就逐渐没落了。

 

参考:

Java进程之间以及跟JVM关系

腾讯面试题04.进程和线程的区别?

并发与并行的区别?

一文秒懂 Java 守护线程 ( Daemon Thread )

线程同步 synchronized 同步代码块 同步方法 同步锁

线程同步及线程锁

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值