线程
通常在一个进程里面可以包含多个线程,但是一个进程里面至少有一个线程。线程可以利用进程拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,把线程作为独立运行和独立调度的基本单位
卖票
这里举例学习Java基础的时候基本都会尝试的卖票练习
//定义一个资源类
class Ticker {
private int ticker = 30;
public synchronized void sale(){
if (ticker>0){
ticker--;
System.out.println(Thread.currentThread().getName()+":"+ticker);
}
}
}
public class test1 {
/**
* 线程操作资源类
* 判断干活通知
* 防止虚假唤醒
* @param args
*/
public static void main(String[] args) {
Ticker ticker = new Ticker();
//三个线程调用卖票方法
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ticker.sale();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ticker.sale();
}
}
},"B").start();
//lambada方式调用
new Thread(()->{
for (int i = 0; i < 15; i++) {
ticker.sale();
}
},"C").start();
}
}
在运行项目后,是可以正常处理完所有操作,并且没有超卖重复卖等问题,这都要归功于synchronized
ReentrantLock
这种写法用到了java.util.concurrent的Lock类,其中里面的ReentrantLock则是实现了Lock接口,通过ReentrantLock也能实现synchronized关键字的效果,他们的区别是
synchronized不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用。ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。 基本的实现方法也比较简单
class Ticker {
private int ticker = 30;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock();
try {
if (ticker>0){
ticker--;
System.out.println(Thread.currentThread().getName()+":"+ticker);
}
}finally {
lock.unlock();
}
}
}
- Lock 是显式锁,synchronized 是隐式锁
- Lock 只有代码块锁,而 synchronized 有代码块锁和方法锁
- Lock 锁会让JVM花较少的时间调度线程,性能更好,子类多扩展性好
- 优先顺序 Lock > synchronized
生产者-消费者
一般的解决思路
先提出一个题目
- 现在两个线程,可以操作初始值为零的一个变量,实现一个线程对该变量加1,一个线程对该变量-1,实现交替,重复10轮,变量初始值为0。
- 首先定义资源类的资源以及操作的方法
class Shop {
private int cake = 0;
public synchronized void add() throws InterruptedException {
while (cake!=0){
this.wait();
}
cake++;
System.out.println(Thread.currentThread().getName()+":"+cake);
this.notifyAll();
}
public synchronized void sub() throws InterruptedException {
while (cake==0){
this.wait();
}
cake--;
System.out.println(Thread.currentThread().getName()+":"+cake);
this.notifyAll();
}
}
这里为什么在判断的时候用while而不是if,举个例子,加入有超过2个线程在操作这个资源类,例如有ABCD四个线程,AC进行add(),BD进行sub()。
- 如果此时变量是0,那么B、D是同时在wait,A和C因为add方法被加了锁,所以只有一个方法进行add
- notifyall之后,此时C线程也会进行add操作,因为此时他已经是在判断条件内部里面了
- 如果使用if,则唤醒后没有继续判断number的情况,同理B,D线程也是。使用while的意义就是,让他重新进行一次判断
这里扩展一个知识点,wait方法和notify方法,查看API我们可以见到这两个方法属于Object的方法,wait 和 notify 必须要配合synchronized 关键字使用。
- 编写线程,这里采用四个
public class _生产者消费者 {
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.sub();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.sub();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
运行之后,没有异常
使用JUC解决
即就是原先我们使用synchronized ,wait,notify,现在使用JUC的lock(ReentrantLock),condition,condition.await(), condition.signalAll();
- 代码如下
/**
* 新版写法
*/
class Shop2 {
private int cake = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void add() throws InterruptedException {
lock.lock();
try {
while (cake!=0){
condition.await();
}
cake++;
System.out.println(Thread.currentThread().getName()+":"+cake);
condition.signalAll();
}finally {
lock.unlock();
}
}
public void sub() throws InterruptedException {
lock.lock();
try {
while (cake==0){
condition.await();
}
cake--;
System.out.println(Thread.currentThread().getName()+":"+cake);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
/**
* 题目:现在两个线程,可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.防止虚假唤醒(判断只能用while,不能用if)
* 知识小总结:多线程编程套路+while判断+新版写法
*/
public class _生产者消费者2 {
public static void main(String[] args) {
Shop2 shop = new Shop2();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.sub();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.add();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
shop.sub();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
那么为什么要使用JUC替代原来的方法呢?新技术的产生会替代就技术解决不了的事情,我们看看下面的例子
精准通知
多个线程之间的调用顺序,实现A->B->C:三个线程的启动顺序如下:AA打印5次,BB打印10次,CC打印15次,持续5轮。
- 主方法
public class _精确通知顺序访问 {
public static void main(String[] args) {
PrintfDemo2 printfDemo = new PrintfDemo2();
new Thread(()->{
for (int i = 0; i < 5; i++) {
printfDemo.printf(1,5);
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
printfDemo.printf(2,10);
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
printfDemo.printf(3,15);
}
},"CC").start();
}
}
- 资源类
class PrintfDemo2{
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private Condition conditions[] = {condition1,condition2,condition3};
public void printf(int signal,int count){
lock.lock();
try {
while (num!=signal){
conditions[signal-1].await();
}
for (int i = 1; i <= count; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
num=num%3+1;
conditions[num-1].signal();
}catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
另一种简单写法
class PrintfDemo{
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void printf5(){
lock.lock();
try {
//判断
while (num!=1){
condition1.await();
}
//操作
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
}
//通知
num = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printf10(){
lock.lock();
try {
//判断
while (num!=2){
condition2.await();
}
//操作
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
}
//通知
num = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printf15(){
lock.lock();
try {
//判断
while (num!=3){
condition3.await();
}
//操作
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
}
//通知
num = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
所以这就是对老技术的优化,精准通知,精准唤醒,通过对指定的condition唤醒来达到对指定的线程的唤醒。