一、并发,并行、进程、线程

并发和并行

并发:我正在吃饭,来电话了,我可以接完电话以后回来接着吃饭。
并行:我可以一边吃饭,一边接电话。

所以关键区别在于:是否同时。
对于单核系统,只能做到并发;对于多核系统,可以做到并行,多个CPU同时处理多件事情。

进程和线程

一个进程内有多个线程

线程调度:

  • 分时调度:轮流使用CPU,平均分配
  • 抢占式调度:优先让优先级高的线程使用CPU,如果优先级相同,则随机分配给某个线程

二、创建线程

方法1:继承Thread类

  1. 需要继承Thread类
  2. 需要重写run方法
  3. 调用时不调用run方法,而是调用start方法 ```java class MyThread extends Thread { @Override public void run() {
    1. for (int i = 0; i < 10; i++) {
    2. System.out.println("新开的线程:" + i);
    3. }
    } }

public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }


<a name="mrdNJ"></a>
## 方法2:实现Runnable接口

1. 类需要实现Runnable接口;
1. 重写run方法;
1. 调用时,先new该类,然后将该类作为new Thread的参数传入;
1. 该Thread对象调用start方法。
```java
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("新线程");
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread myThread = new Thread(myRunnable);

        myThread.start();
    }
}

实现Runnable接口通常比继承Thread类更好。

三、常用方法

// 主动休眠
public static void sleep(long millis)
// 主动放弃时间片,回到就绪状态
public static void yield()
// 允许其他线程加入到当前线程
public final void join()
// 设置优先级,1-10,默认为5
public void setPriority(int)
// 设置为守护进程
public void setDaemon(boolean)

优先级

优先级越高,获得较多的运行机会。只能反映线程的紧急程度,不能决定是否一定先执行。

休眠

Thread.sleep(1000);

是静态方法,在哪个线程里调用就让哪个线程休眠,例如,在main方法里调用Thread.sleep(1000); 就会让main线程睡眠。

让步

Thread.yield()

静态方法,让步给其他线程,但并不能保证其他线程先执行完再执行该线程。

sleep和yield的区别
sleep的线程,指定时间内一定不会被执行。
yield的线程,只是进入到可执行状态,有可能马上又被执行。

线程的合并

即在线程A,线程B调用join方法, 那么线程A需要等待线程B完全执行结束以后,才能接着执行线程A。
通常需要在A需要用到B中的结果才使用join方法。

package com.tuling.part1;

class JoinThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Join线程:" + i);
        }
    }
}

public class TestJoin {
    public static void main(String[] args) {
        System.out.println("主线程开始");
        Thread joinThread = new JoinThread();
        joinThread.start();

        try {
            // joinThread执行结束以后,主线程才能接着执行。
            joinThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}

四、守护线程

守护线程就是,如果在线程A里有守护线程B,A线程如果结束了,即便线程B没有结束,也将随着A线程结束,这就是“守护”

方式:
deamonThread.setDeamon();

class DeamonThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("守护线程: " + i);
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

public class TestDeamon {
    public static void main(String[] args) {
        Thread deamonThread = new DeamonThread();
        deamonThread.setDaemon(true);
        deamonThread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程:" + i);
        }
        System.out.println("主线程结束啦");
    }
}

以上代码,守护线程并不会输出100次,因为主线程结束后,守护线程也随着结束了。

五、线程的生命周期

image.png

六、线程安全

为什么会出现线程安全问题?
当多线程时,CPU在多个线程间高速切换,当涉及写操作时,可能出现写还未完成,又被另一个线程读取的情况,导致数据的不统一。

解决方案?

  1. 同步代码块;
  2. 同步方法;
  3. 锁。

同步代码块

即用关键字synchronized来将有可能出现线程安全问题的代码包裹起来,每次只允许一个线程进来执行其中的代码。

package com.tuling.part1;


class Ticket implements Runnable {
    private int tickets = 100;
    private Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets-- + "张票");
                }
            }
        }
    }
}


public class TestSychronized {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

可以用任意类型的对象来作为锁,例如上述代码可以用this作为锁,因为这个runnable的对象只有唯一的一个。
但是,多个线程,必须使用同一把锁!!!

同步方法

即用sychronized修饰一个方法,使其成为一个同步方法。
不需要手动上锁,如果是非静态方法会默认使用this作为锁,如果是静态方法,默认使用类名.class作为锁。

package com.tuling.part1;

class Ticket1 implements Runnable {
    private int tickets = 10000;

    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    public synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets-- + "张票");
        }
    }
}

public class TestSychronizedMethod {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1();
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t3.setPriority(1);

        t1.start();
        t2.start();
        t3.start();
    }
}

  • JDK5加入,更加灵活
  • 提供更多实用方法,功能更强大,性能更优越

方式:

  1. 先new一个锁对象;
  2. lock.lock();锁住
  3. 结束后,lock.unlock();释放

ps: 一定要用try finally包裹住,在finally里中释放锁,否则如果发生异常,锁永远释放不了导致死锁。

class Ticket2 implements Runnable {
    private int tickets = 1000;
    Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while (true) {
            lock.lock();
            try {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets-- + "张票");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

public class TestLock {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

七、线程通信

有时候,我们需要多个线程按照顺序去执行,就需要用到线程通信。我们用等待唤醒机制来解决。

  1. 用sychronized来构建同步代码块,并加锁 (任意object即可)。
  2. lock.wait()进入等待;
  3. 在其他线程里通过lock.notify()来唤醒等待的线程。
  4. 或者通过lock.notifyAll()来唤醒所有等待的线程。

八、死锁

两个线程,各自在等对方释放锁,就会发生死锁。对于多个线程也是一样。
如下代码:
A拿到了lock1, B拿到了lock2, 接下来B先醒来(因为只睡了1s),想要去拿lock1,但是拿不到,因为被A占着。
然后A醒来,要去拿lock2, 但是拿不到,因为被B占着。于是两个线程各自等待对方释放锁,导致发生死锁。

public class TestDeadLock {
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock1) {
                    System.out.println("A完成了lock1");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2) {
                        System.out.println("A完成了lock2");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock2) {
                    System.out.println("B完成了lock2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock1) {
                        System.out.println("B完成了lock1");
                    }
                }
            }
        }).start();
    }
}

九、线程池

因为频繁的生成和销毁线程效率不高,因此使用线程池来解决这个问题。

线程池的使用步骤:

  1. 创建线程池对象:

    // 使用Executors,创建包含3个线程的线程池。
    ExecutorService pool = Executors.newFixedThreadPool(3);
    
  2. 创建Runnable接口子类对象;

    MyRunnable myRunnable = new MyRunnable();
    
  3. 提交Runnable接口子类对象:

    pool.submit(myRunnable);
    

    完整代码如下: ```java package com.tuling.part1;

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

class MyRunnable1 implements Runnable { @Override public void run() { System.out.println(“——我需要一个游泳教练——“); System.out.println(“——游泳——-“); System.out.println(“——游泳结束,回去了——“); } }

public class TestThreadPool { public static void main(String[] args) { // 生成线程池对象 ExecutorService pool = Executors.newFixedThreadPool(3);

    // 生成Runnable对象
    MyRunnable1 myRunnable1 = new MyRunnable1();

    // 提交Runnable对象
    pool.submit(myRunnable1);
    pool.submit(myRunnable1);
    pool.submit(myRunnable1);
    pool.submit(myRunnable1);
    pool.submit(myRunnable1);

}

}

ps: 虽然线程池中只有3个可用线程,但是可以提交数 > 3, 因为会先提交3个,执行完毕后,线程回到线程池,发现还有任务未完成,就会接着去执行任务。这样就避免了反复创建线程的过程。

<a name="yYdJi"></a>
## 创建线程池对象的多种方式:

1. newFixedThreadPool(int); 创建固定长度的线程池
1. newCachedThreadPool(); 创建可伸缩的线程池,不用指定数量。
1. newSingleThreadPoolExecutor(); 创建单线程的Executor
1. newScheduledThreadPool(int), 固定长度的线程池,而且以延迟或者定时来执行,类似Timer;

<a name="lpgig"></a>
## Callable接口
相比于Runnable接口,Callable接口可以有返回值。但是需要依靠线程池,返回值是一个Future对象。

1. 生成线程池对象;
1. 生成Callable对象,需要实现call方法;
1. 提交Callable对象,得到Future类型的返回值;
1. 通过future.get()方法获取返回值。

```java
package com.tuling.part1;

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return (int)(Math.random() * 10);
    }
}

public class TestCallable {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        MyCallable myCallable = new MyCallable();
        Future<Integer> future = pool.submit(myCallable);
        Future<Integer> future1 = pool.submit(myCallable);
        Future<Integer> future2 = pool.submit(myCallable);

        try {
            System.out.println(future.get());
            System.out.println(future1.get());
            System.out.println(future2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        pool.shutdown();

    }
}

十、线程安全集合

CopyOnWriteArray

该集合是线程安全的,用来替换ArrayList。
如果在多线程的情况下对ArrayList进行写入,将会出现线程安全问题,例如写入被覆盖或者不成功。因此需要使用CopyOnWriteArray来解决。它在写入的时候加了锁,防止出现线程安全问题。

CopyOnWriteArraySet

该集合用来替换HashSet

CocurrentHashMap

用来替换HashMap,不是对整个map加锁,而是对每个segment加锁。
默认情况下有16个segment, 就是对底层数组中的每个元素加锁。