1. 进程与线程
1.1 进程
- 指正在运行的程序,是系统进行资源分配的基本单位。
- 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分。
- 单核CPU在同一个时刻,只能运行一个进程;宏观并行、微观串行。
1.2 线程
- 可以理解为应用程序中不同的执行路径。
- 是CPU的基本调度单位。
- 一个进程由一个或者多个线程组成,彼此间完成不同的工作。同时执行则称为多线程。
- 例如:迅雷是一个进程,迅雷中的多个下载任务即是多个线程。Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。
1.3 进程与线程的区别
- 进程是操作系统资源分配的基本单位,而线程则是CPU的基本调度单位
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
- 进程之间不能共享数据段地址,但是线程之间可以。
1.4 线程的组成部分
1、CPU时间片:操作系统(OS)会为每个线程分配执行时间。
2、运行数据:
堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
3、线程的逻辑代码。
1.5 线程的特点
1、线程抢占式执行:效率高、可防止单一线程长时间占用CPU
2、在单核CPU中,宏观上同时执行,微观上顺序执行。
1.6 线程优先级
1、设置线程优先级:new Thread().setPriority();
2、线程优先级为1-10。默认为5,优先级越高表示获取CPU机会越多。
1.7 守护线程
1、设置线程为守护线程: new Thread.setDaemon(true);
2、线程有两类:用户线程(前台线程)、守护线程(后台线程);创建线程时默认为前台线程。
3、如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
4、垃圾回收器线程属于守护线程。
1.8 创建线程的三种方式
1、class A类继承Thread类,重写run方法,class A 直接调用start方法启动线程。
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
public static void main(String[] args) {
new MyThread().start();
}
2、class B 实现Runnable接口,重写run方法,通过new Thread(new class B)调用start方法启动线程。
static class MyRun implements Runnable{
@Override
public void run() {
System.out.println("Hello MyRun!");
}
}
public static void main(String[] args) {
new Thread(new MyRun()).start();
}
3、实现Callable接口,Callable具有泛型返回值、可以声明异常。通过FutureTask类将Callable对象转为可执行任务,然后使用Thread类创建线程,将futureTask提交给线程。
public static void main(String[] args) throws Exception {
//功能需求 使用Callable实现求1-100的和
//1、创建Callable对象
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
};
//2、把Callable对象转成可执行任务
FutureTask<Integer> task = new FutureTask<>(callable);
//3、创建线程
Thread thread = new Thread(task);
//4、启动线程
thread.start();
//5.获取结果(等待call指向完毕,才会返回)
Integer sum = task.get();
System.out.println("结果是:" + sum);
}
1.9 线程的几个基本方法
1、sleep() : 休眠,当前线程主动休眠 millis毫秒。
static void testSleep() {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
2、yield() :当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
static void testYield() {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
if (i % 10 == 0) Thread.yield();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("B" + i);
if (i % 10 == 0) Thread.yield();
}
}).start();
}
3、join() : 让“主线程”等待“子线程”结束之后才能继续运行。允许其他线程加入到当前线程中。加入当前线程,并阻塞当前线程,直到加入线程执行完毕。
通过此方法可以让几个线程按照特定顺序执行。
static void testJoin() {
Thread t1=new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
try {
t1.join();
//调用join过后,只有当t1线程运行完过后,t2线程才能开始运行,否则t1,t2是同时运行的。
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("B" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
1.10 线程状态
通过Thread类中的State枚举得知线程有六种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
- RUNNABLE:线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
- BLOCKED:由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
- WAITING:线程无限期等待唤醒。
- TIMED_WAITING:线程在等待唤醒,并且设置了等待时限。
- TERMINATED:当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

2.线程安全问题
当多线程并发访问临界资源时,如果破坏原子操作,可能会导致数据不一致。
临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省。
那么在应用程序中,如何保证线程的安全性?
2.1 使用同步代码块
synchronized(临界资源对象) { //对临界资源对象加锁
//代码 (原子操作)
}
两个线程 a,b 当线程a访问临界资源后,由于加了锁,线程b则不能再访问临界资源,当a执行完后,锁会自动释放,线程b可以访问临界资源。
对临界资源对象加锁过后,同一时刻,只能有一个线程进行原子操作,别的线程都要等待。
示例代码:
private static int index = 0;
public static void main(String[] args) throws InterruptedException {
//创建数组
String[] s = new String[5];
//创建两个操作
Runnable runnableA = new Runnable() {
@Override
public void run() {
//同步代码块
synchronized (s) {
s[index] = "hello";
index++;
}
}
};
Runnable runnableB = new Runnable() {
@Override
public void run() {
synchronized (s) {
s[index] = "world";
index++;
}
}
};
//创建两个线程
Thread threadA = new Thread(runnableA, "A");
Thread threadB = new Thread(runnableB, "B");
//启动线程
threadA.start();
threadB.start();
//加入主线程,保证后面的打印操作执行时两个线程执行完毕
threadA.join();
threadB.join();
System.out.println(Arrays.toString(s));
}
注意:synchronized (object) :不能用synchronized (new Object()),这样创建的就不是同一把锁,可以用synchronized (this) this表示当前对象,即原子操作所操作的对象。每个对象都有一个互斥锁标记,用来分配给线程的。只有拥有对象对象互斥锁标记的线程,才能进入该对象加锁的同步代码块。线程退出同步代码块时,会释放相应的互斥锁标记。
2.2 使用同步方法
synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁
//代码(原子操作)
}
示例代码:
/**
* @ClassName Ticket
* @Description 票类(共享资源)
*/
public class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (!sale()){
break;
}
}
}
//买票(同步方法)
private synchronized boolean sale(){ //锁---this :当前对象Ticket
if (ticket<=0){
return false;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
ticket--;
return true;
}
}
public static void main(String[] args) {
//1、创建Ticket对象
Ticket ticket=new Ticket();
//2、创建线程对象
Thread t1=new Thread(ticket,"窗口1");
Thread t2=new Thread(ticket,"窗口2");
Thread t3=new Thread(ticket,"窗口3");
Thread t4=new Thread(ticket,"窗口4");
//3、创建线程
t1.start();
t2.start();
t3.start();
t4.start();
}
注意:对方法上锁时,如果时非静态方法,锁---this 。 如果是静态方法,锁class。
2.3 同步规则
- 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
- 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
已知JDK中线程安全的类:
1、StringBuffer 2、Vector 3、HashTable
以上类中的公开方法,均为synchonized修饰的同步方法。
2.4 死锁
1、当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
2、一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
例如:生活中一个男孩和一个女孩吃东西,但只有一双筷子,需要两只同时拥有才能吃东西,但男孩和女孩手中一人手中只有一只筷子,谁也不肯让谁,那么此时谁也吃不了东西,造成死锁。
代码示例:
public class MyLock {
//两个锁 ---两根筷子
public static Object a=new Object();
public static Object b=new Object();
}
public class Girl extends Thread {
@Override
public void run() {
synchronized (MyLock.b){
System.out.println("女孩拿到了b");
synchronized (MyLock.a){
System.out.println("女孩拿到了a");
System.out.println("女孩可以吃东西了。。。。。");
}
}
}
}
public class Boy extends Thread {
@Override
public void run() {
synchronized (MyLock.a){
System.out.println("男孩拿到了a");
synchronized (MyLock.b){
System.out.println("男孩拿到了b");
System.out.println("男孩可以吃东西了。。。。。");
}
}
}
}
public static void main(String[] args) {
Boy boy=new Boy();
Girl girl=new Girl();
girl.start();
boy.start();
}
上述代码中,对筷子上了锁,必须要同时拿到两根筷子才能吃东西。而由于线程具有抢占式执行的特点,极有可能男孩和女孩一人抢了一根筷子,造成死锁。那么如何避免此种情况发生呢?其实很简单,代码如下:
public static void main(String[] args) {
Boy boy=new Boy();
Girl girl=new Girl();
girl.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
boy.start();
}
两个线程启动时,先启动其中一个,然后休眠一会,保证先启动那个能抢到两只筷子,然后再启动另一个线程。这样就不会造成死锁。
本文介绍了进程与线程的基础概念,包括它们的区别、线程的创建方式及特点等,并深入探讨了线程安全问题及其解决方案。





